Forking Language Server in the Project Manager (#1305)

This commit is contained in:
Radosław Waśko 2020-12-02 16:56:47 +01:00 committed by GitHub
parent a40989e7c6
commit 9e1b49d245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 2685 additions and 1686 deletions

View File

@ -133,6 +133,11 @@ jobs:
# test run. It is built before starting the tests to conserve system
# memory
echo "LAUNCHER_NATIVE_IMAGE_TEST_SKIP_BUILD=true" >> $GITHUB_ENV
- name: Build the Runner & Runtime Uberjars
run: |
sleep 1
sbt --no-colors engine-runner/assembly
- name: Test Enso
run: |
sleep 1
@ -151,10 +156,6 @@ jobs:
sbt --no-colors searcher/Benchmark/compile
# Build Distribution
- name: Build the Runner & Runtime Uberjars
run: |
sleep 1
sbt --no-colors engine-runner/assembly
- name: Build the Project Manager Uberjar
run: |
sleep 1

View File

@ -609,6 +609,26 @@ lazy val `logging-service` = project
)
.dependsOn(`akka-native`)
ThisBuild / testOptions += Tests.Setup(_ =>
// Note [Logging Service in Tests]
sys.props("org.enso.loggingservice.test-log-level") = "2"
)
/* Note [Logging Service in Tests]
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* As migrating the runner to our new logging service has forced us to migrate
* other components that are related to it too, some tests that used to be
* configured by logback are not properly configured anymore and log a lot of
* debug information. This is a temporary fix to make sure that there are not
* too much logs in the CI - it sets the log level for all tests to be at most
* info by default. It can be overridden by particular tests if they set up a
* logging service.
*
* This is a temporary solution and it will be obsoleted once all of our
* components do a complete migration to the new logging service, which is
* planned in tasks #1144 and #1151.
*/
lazy val cli = project
.in(file("lib/scala/cli"))
.configs(Test)
@ -647,15 +667,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
(Compile / run / fork) := true,
(Test / fork) := true,
(Compile / run / connectInput) := true,
javaOptions ++= {
// Note [Classpath Separation]
val runtimeClasspath =
(runtime / Compile / fullClasspath).value
.map(_.data)
.mkString(File.pathSeparator)
Seq(s"-Dtruffle.class.path.append=$runtimeClasspath")
},
libraryDependencies ++= akka,
libraryDependencies ++= akka ++ Seq(akkaTestkit % Test),
libraryDependencies ++= circe,
libraryDependencies ++= Seq(
"com.typesafe" % "config" % typesafeConfigVersion,
@ -665,6 +677,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
"dev.zio" %% "zio-interop-cats" % zioInteropCatsVersion,
"commons-cli" % "commons-cli" % commonsCliVersion,
"commons-io" % "commons-io" % commonsIoVersion,
"org.apache.commons" % "commons-lang3" % commonsLangVersion,
"com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion,
"com.miguno.akka" %% "akka-mock-scheduler" % akkaMockSchedulerVersion % Test,
"org.mockito" %% "mockito-scala" % mockitoScalaVersion % Test
@ -697,13 +710,12 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
)
)
),
assembly := assembly
.dependsOn(runtime / assembly)
.value
assembly := assembly.dependsOn(`engine-runner` / assembly).value,
(Test / test) := (Test / test).dependsOn(`engine-runner` / assembly).value
)
.dependsOn(`version-output`)
.dependsOn(pkg)
.dependsOn(`language-server`)
.dependsOn(`polyglot-api`)
.dependsOn(`runtime-version-manager`)
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
@ -867,8 +879,7 @@ lazy val `polyglot-api` = project
lazy val `language-server` = (project in file("engine/language-server"))
.settings(
libraryDependencies ++= akka ++ akkaTest ++ circe ++ Seq(
"ch.qos.logback" % "logback-classic" % logbackClassicVersion,
libraryDependencies ++= akka ++ circe ++ Seq(
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"io.circe" %% "circe-generic-extras" % circeGenericExtrasVersion,
"io.circe" %% "circe-literal" % circeVersion,
@ -902,6 +913,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
.dependsOn(`text-buffer`)
.dependsOn(`searcher`)
.dependsOn(testkit % Test)
.dependsOn(`logging-service`)
lazy val ast = (project in file("lib/scala/ast"))
.settings(

View File

@ -261,11 +261,6 @@ The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.google.auto.service.auto-service-annotations-1.0-rc7`.
'logback-classic', licensed under the GNU Lesser General Public License, is distributed with the engine.
The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `ch.qos.logback.logback-classic-1.2.3`.
'scala-collection-compat_2.13', licensed under the Apache-2.0, is distributed with the engine.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `org.scala-lang.modules.scala-collection-compat_2.13-2.0.0`.
@ -466,11 +461,6 @@ The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `com.beachape.enumeratum_2.13-1.6.1`.
'logback-core', licensed under the GNU Lesser General Public License, is distributed with the engine.
The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `ch.qos.logback.logback-core-1.2.3`.
'pureconfig_2.13', licensed under the Mozilla Public License, version 2.0, is distributed with the engine.
The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `com.github.pureconfig.pureconfig_2.13-0.13.0`.

View File

@ -1,259 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- saved from url=(0042)https://www.eclipse.org/legal/epl-v10.html -->
<html xmlns="http://www.w3.org/1999/xhtml"><link type="text/css" rel="stylesheet" id="dark-mode-general-link"><link type="text/css" rel="stylesheet" id="dark-mode-custom-link"><style type="text/css" id="dark-mode-custom-style"></style><head><meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<title>Eclipse Public License - Version 1.0</title>
<style type="text/css">
body {
size: 8.5in 11.0in;
margin: 0.25in 0.5in 0.25in 0.5in;
tab-interval: 0.5in;
}
p {
margin-left: auto;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
p.list {
margin-left: 0.5in;
margin-top: 0.05em;
margin-bottom: 0.05em;
}
</style>
</head>
<body lang="EN-US">
<h2>Eclipse Public License - v 1.0</h2>
<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
AGREEMENT.</p>
<p><b>1. DEFINITIONS</b></p>
<p>"Contribution" means:</p>
<p class="list">a) in the case of the initial Contributor, the initial
code and documentation distributed under this Agreement, and</p>
<p class="list">b) in the case of each subsequent Contributor:</p>
<p class="list">i) changes to the Program, and</p>
<p class="list">ii) additions to the Program;</p>
<p class="list">where such changes and/or additions to the Program
originate from and are distributed by that particular Contributor. A
Contribution 'originates' from a Contributor if it was added to the
Program by such Contributor itself or anyone acting on such
Contributor's behalf. Contributions do not include additions to the
Program which: (i) are separate modules of software distributed in
conjunction with the Program under their own license agreement, and (ii)
are not derivative works of the Program.</p>
<p>"Contributor" means any person or entity that distributes
the Program.</p>
<p>"Licensed Patents" mean patent claims licensable by a
Contributor which are necessarily infringed by the use or sale of its
Contribution alone or when combined with the Program.</p>
<p>"Program" means the Contributions distributed in accordance
with this Agreement.</p>
<p>"Recipient" means anyone who receives the Program under
this Agreement, including all Contributors.</p>
<p><b>2. GRANT OF RIGHTS</b></p>
<p class="list">a) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free copyright license to reproduce, prepare derivative works
of, publicly display, publicly perform, distribute and sublicense the
Contribution of such Contributor, if any, and such derivative works, in
source code and object code form.</p>
<p class="list">b) Subject to the terms of this Agreement, each
Contributor hereby grants Recipient a non-exclusive, worldwide,
royalty-free patent license under Licensed Patents to make, use, sell,
offer to sell, import and otherwise transfer the Contribution of such
Contributor, if any, in source code and object code form. This patent
license shall apply to the combination of the Contribution and the
Program if, at the time the Contribution is added by the Contributor,
such addition of the Contribution causes such combination to be covered
by the Licensed Patents. The patent license shall not apply to any other
combinations which include the Contribution. No hardware per se is
licensed hereunder.</p>
<p class="list">c) Recipient understands that although each Contributor
grants the licenses to its Contributions set forth herein, no assurances
are provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity. Each
Contributor disclaims any liability to Recipient for claims brought by
any other entity based on infringement of intellectual property rights
or otherwise. As a condition to exercising the rights and licenses
granted hereunder, each Recipient hereby assumes sole responsibility to
secure any other intellectual property rights needed, if any. For
example, if a third party patent license is required to allow Recipient
to distribute the Program, it is Recipient's responsibility to acquire
that license before distributing the Program.</p>
<p class="list">d) Each Contributor represents that to its knowledge it
has sufficient copyright rights in its Contribution, if any, to grant
the copyright license set forth in this Agreement.</p>
<p><b>3. REQUIREMENTS</b></p>
<p>A Contributor may choose to distribute the Program in object code
form under its own license agreement, provided that:</p>
<p class="list">a) it complies with the terms and conditions of this
Agreement; and</p>
<p class="list">b) its license agreement:</p>
<p class="list">i) effectively disclaims on behalf of all Contributors
all warranties and conditions, express and implied, including warranties
or conditions of title and non-infringement, and implied warranties or
conditions of merchantability and fitness for a particular purpose;</p>
<p class="list">ii) effectively excludes on behalf of all Contributors
all liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;</p>
<p class="list">iii) states that any provisions which differ from this
Agreement are offered by that Contributor alone and not by any other
party; and</p>
<p class="list">iv) states that source code for the Program is available
from such Contributor, and informs licensees how to obtain it in a
reasonable manner on or through a medium customarily used for software
exchange.</p>
<p>When the Program is made available in source code form:</p>
<p class="list">a) it must be made available under this Agreement; and</p>
<p class="list">b) a copy of this Agreement must be included with each
copy of the Program.</p>
<p>Contributors may not remove or alter any copyright notices contained
within the Program.</p>
<p>Each Contributor must identify itself as the originator of its
Contribution, if any, in a manner that reasonably allows subsequent
Recipients to identify the originator of the Contribution.</p>
<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
<p>Commercial distributors of software may accept certain
responsibilities with respect to end users, business partners and the
like. While this license is intended to facilitate the commercial use of
the Program, the Contributor who includes the Program in a commercial
product offering should do so in a manner which does not create
potential liability for other Contributors. Therefore, if a Contributor
includes the Program in a commercial product offering, such Contributor
("Commercial Contributor") hereby agrees to defend and
indemnify every other Contributor ("Indemnified Contributor")
against any losses, damages and costs (collectively "Losses")
arising from claims, lawsuits and other legal actions brought by a third
party against the Indemnified Contributor to the extent caused by the
acts or omissions of such Commercial Contributor in connection with its
distribution of the Program in a commercial product offering. The
obligations in this section do not apply to any claims or Losses
relating to any actual or alleged intellectual property infringement. In
order to qualify, an Indemnified Contributor must: a) promptly notify
the Commercial Contributor in writing of such claim, and b) allow the
Commercial Contributor to control, and cooperate with the Commercial
Contributor in, the defense and any related settlement negotiations. The
Indemnified Contributor may participate in any such claim at its own
expense.</p>
<p>For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those
performance claims and warranties, and if a court requires any other
Contributor to pay any damages as a result, the Commercial Contributor
must pay those damages.</p>
<p><b>5. NO WARRANTY</b></p>
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
responsible for determining the appropriateness of using and
distributing the Program and assumes all risks associated with its
exercise of rights under this Agreement , including but not limited to
the risks and costs of program errors, compliance with applicable laws,
damage to or loss of data, programs or equipment, and unavailability or
interruption of operations.</p>
<p><b>6. DISCLAIMER OF LIABILITY</b></p>
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
<p><b>7. GENERAL</b></p>
<p>If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further action
by the parties hereto, such provision shall be reformed to the minimum
extent necessary to make such provision valid and enforceable.</p>
<p>If Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the
Program itself (excluding combinations of the Program with other
software or hardware) infringes such Recipient's patent(s), then such
Recipient's rights granted under Section 2(b) shall terminate as of the
date such litigation is filed.</p>
<p>All Recipient's rights under this Agreement shall terminate if it
fails to comply with any of the material terms or conditions of this
Agreement and does not cure such failure in a reasonable period of time
after becoming aware of such noncompliance. If all Recipient's rights
under this Agreement terminate, Recipient agrees to cease use and
distribution of the Program as soon as reasonably practicable. However,
Recipient's obligations under this Agreement and any licenses granted by
Recipient relating to the Program shall continue and survive.</p>
<p>Everyone is permitted to copy and distribute copies of this
Agreement, but in order to avoid inconsistency the Agreement is
copyrighted and may only be modified in the following manner. The
Agreement Steward reserves the right to publish new versions (including
revisions) of this Agreement from time to time. No one other than the
Agreement Steward has the right to modify this Agreement. The Eclipse
Foundation is the initial Agreement Steward. The Eclipse Foundation may
assign the responsibility to serve as the Agreement Steward to a
suitable separate entity. Each new version of the Agreement will be
given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version
of the Agreement is published, Contributor may elect to distribute the
Program (including its Contributions) under the new version. Except as
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
rights or licenses to the intellectual property of any Contributor under
this Agreement, whether expressly, by implication, estoppel or
otherwise. All rights in the Program not expressly granted under this
Agreement are reserved.</p>
<p>This Agreement is governed by the laws of the State of New York and
the intellectual property laws of the United States of America. No party
to this Agreement will bring a legal action under this Agreement more
than one year after the cause of action arose. Each party waives its
rights to a jury trial in any resulting litigation.</p>
</body></html>

View File

@ -1,16 +0,0 @@
Logback: the reliable, generic, fast and flexible logging framework.
Copyright (C) 1999-2017, QOS.ch. All rights reserved.
This program and the accompanying materials are dual-licensed under
either the terms of the Eclipse Public License v1.0 as published by
the Eclipse Foundation
or (per the licensee's choosing)
under the terms of the GNU Lesser General Public License version 2.1
as published by the Free Software Foundation.
------------------
We have chosen EPL in this project.

View File

@ -1 +0,0 @@
Please see ch.qos.logback.logback-classic-1.2 for notices related to logback.

View File

@ -116,6 +116,12 @@ The review can be performed manually by modifying the settings inside of the
#### Review Process
> The updates performed using the web script are remembered locally, so they
> **will not show up after the refresh**. If you ever need to open the edit mode
> after closing its window, you should re-generate the report using
> `enso/gatherLicenses` or just open it using `enso/openLegalReviewReport` which
> will refresh it automatically.
1. Open the review in edit mode using the helper script.
- You can type `enso / openLegalReviewReport` if you have `npm` in your PATH
as visible from SBT.
@ -192,12 +198,6 @@ The review can be performed manually by modifying the settings inside of the
- Ensure that there are no more warnings, and if there are any go back to fix
the issues.
The updates performed using the web script are remembered locally, so they will
not show up after the refresh. If you ever need to open the edit mode after
closing its window, you should re-generate the report using
`enso/gatherLicenses` or just open it using `enso/openLegalReviewReport` which
will refresh it automatically.
#### Additional Manual Considerations
The Scala Library notice contains the following mention:

View File

@ -65,6 +65,8 @@ transport formats, please look [here](./protocol-architecture).
- [`executionContext/canModify`](#executioncontextcanmodify)
- [`executionContext/receivesUpdates`](#executioncontextreceivesupdates)
- [`search/receivesSuggestionsDatabaseUpdates`](#searchreceivessuggestionsdatabaseupdates)
- [Enables](#enables-4)
- [Disables](#disables-4)
- [File Management Operations](#file-management-operations)
- [`file/write`](#filewrite)
- [`file/read`](#fileread)
@ -94,6 +96,9 @@ transport formats, please look [here](./protocol-architecture).
- [`workspace/redo`](#workspaceredo)
- [Monitoring](#monitoring)
- [`heartbeat/ping`](#heartbeatping)
- [`heartbeat/init`](#heartbeatinit)
- [Refactoring](#refactoring)
- [`refactoring/renameProject`](#refactoringrenameproject)
- [Execution Management Operations](#execution-management-operations)
- [Execution Management Example](#execution-management-example)
- [Create Execution Context](#create-execution-context)
@ -113,9 +118,9 @@ transport formats, please look [here](./protocol-architecture).
- [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation)
- [`executionContext/visualisationUpdate`](#executioncontextvisualisationupdate)
- [Search Operations](#search-operations)
- [Suggestions Database Example](#suggestionsdatabaseexample)
- [Suggestions Database Example](#suggestions-database-example)
- [`search/getSuggestionsDatabase`](#searchgetsuggestionsdatabase)
- [`search/invalidateSuggestionsDatabase`](#invalidatesuggestionsdatabase)
- [`search/invalidateSuggestionsDatabase`](#searchinvalidatesuggestionsdatabase)
- [`search/getSuggestionsDatabaseVersion`](#searchgetsuggestionsdatabaseversion)
- [`search/suggestionsDatabaseUpdate`](#searchsuggestionsdatabaseupdate)
- [`search/completion`](#searchcompletion)
@ -124,17 +129,17 @@ transport formats, please look [here](./protocol-architecture).
- [`io/redirectStandardOutput`](#ioredirectstdardoutput)
- [`io/suppressStandardOutput`](#iosuppressstdardoutput)
- [`io/standardOutputAppended`](#iostandardoutputappended)
- [`io/redirectStandardError`](#ioredirectstdarderror)
- [`io/suppressStandardError`](#iosuppressstdarderror)
- [`io/redirectStandardError`](#ioredirectstandarderror)
- [`io/suppressStandardError`](#iosuppressstandarderror)
- [`io/standardErrorAppended`](#iostandarderrorappended)
- [`io/feedStandardInput`](#iofeedstandardinput)
- [`io/waitingForStandardInput`](#iowaitingforstandardinput)
- [Errors](#errors)
- [Errors](#errors-57)
- [`AccessDeniedError`](#accessdeniederror)
- [`FileSystemError`](#filesystemerror)
- [`ContentRootNotFoundError`](#contentrootnotfounderror)
- [`FileNotFound`](#filenotfound)
- [`FileExists`](#fileexists-1)
- [`FileExists`](#fileexists)
- [`OperationTimeoutError`](#operationtimeouterror)
- [`NotDirectory`](#notdirectory)
- [`StackItemNotFoundError`](#stackitemnotfounderror)
@ -145,7 +150,6 @@ transport formats, please look [here](./protocol-architecture).
- [`VisualisationNotFoundError`](#visualisationnotfounderror)
- [`VisualisationExpressionError`](#visualisationexpressionerror)
- [`VisualisationEvaluationError`](#visualisationevaluationerror)
- [`ExecutionFailedError`](#executionfailederror)
- [`FileNotOpenedError`](#filenotopenederror)
- [`TextEditValidationError`](#texteditvalidationerror)
- [`InvalidVersionError`](#invalidversionerror)
@ -154,6 +158,8 @@ transport formats, please look [here](./protocol-architecture).
- [`SessionNotInitialisedError`](#sessionnotinitialisederror)
- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror)
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror)
- [`ProjectNotFoundError`](#projectnotfounderror)
- [`ModuleNameNotResolvedError`](#modulenamenotresolvederror)
<!-- /MarkdownTOC -->
@ -2085,6 +2091,33 @@ null;
None
### `heartbeat/init`
This request is sent from the bootloader to check if the started language server
instance has finished initialization. A reply should only be sent when the main
module has been fully initialized.
- **Type:** Request
- **Direction:** Supervisor -> Server
- **Connection:** Protocol
- **Visibility:** Private
#### Parameters
```typescript
null;
```
#### Result
```typescript
null;
```
#### Errors
None
## Refactoring
The language server also provides refactoring operations to restructure an

View File

@ -65,6 +65,7 @@ transport formats, please look [here](./protocol-architecture.md).
- [`ProjectCloseError`](#projectcloseerror)
- [`LanguageServerError`](#languageservererror)
- [`GlobalConfigurationAccessError`](#globalconfigurationaccesserror)
- [`ProjectCreateError`](#projectcreateerror)
- [`LoggingServiceUnavailable`](#loggingserviceunavailable)
<!-- /MarkdownTOC -->
@ -143,8 +144,8 @@ operation also includes spawning an instance of the language server open on the
specified project.
To open a project, an engine version that is specified in project settings needs
to be installed. If `missingComponentAction` is set to `install` or
`force-install-broken`, this action will install any missing components,
to be installed. If `missingComponentAction` is set to `Install` or
`ForceInstallBroken`, this action will install any missing components,
otherwise, an error will be reported if a component is missing. A typical usage
scenario may consist of first trying to open the project without installing
missing components. If that fails with the `MissingComponentError`, the client
@ -165,7 +166,7 @@ interface ProjectOpenRequest {
/**
* Specifies how to handle missing components.
*
* If not provided, defaults to `fail`.
* If not provided, defaults to `Fail`.
*/
missingComponentAction?: MissingComponentAction;
}
@ -303,7 +304,7 @@ interface ProjectCreateRequest {
/**
* Specifies how to handle missing components.
*
* If not provided, defaults to `fail`.
* If not provided, defaults to `Fail`.
*/
missingComponentAction?: MissingComponentAction;
}
@ -1045,13 +1046,24 @@ Signals that the global configuration file could not be accessed or parsed.
}
```
### `ProjectCreateError`
Signals that an error occurred when creating the project.
```typescript
"error" : {
"code" : 4012,
"message" : "Could not create the project."
}
```
### `LoggingServiceUnavailable`
Signals that the logging service is not available.
```typescript
"error" : {
"code" : 4012,
"code" : 4013,
"message" : "The logging service has failed to boot."
}
```

View File

@ -1,16 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%-15thread] %-36logger{36} %msg%n</pattern>
</encoder>
</appender>
<logger name="org.enso" level="TRACE"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -14,6 +14,7 @@ import org.enso.languageserver.runtime.RuntimeKiller.{
RuntimeShutdownResult,
ShutDownRuntime
}
import org.enso.loggingservice.LogLevel
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
@ -21,8 +22,9 @@ import scala.concurrent.{Await, Future}
/** A lifecycle component used to start and stop a Language Server.
*
* @param config a LS config
* @param logLevel log level for the Language Server
*/
class LanguageServerComponent(config: LanguageServerConfig)
class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
extends LifecycleComponent
with LazyLogging {
@ -34,7 +36,7 @@ class LanguageServerComponent(config: LanguageServerConfig)
/** @inheritdoc */
override def start(): Future[ComponentStarted.type] = {
logger.info("Starting Language Server...")
val module = new MainModule(config)
val module = new MainModule(config, logLevel)
val initMainModule =
for {
_ <- module.init

View File

@ -2,7 +2,6 @@ package org.enso.languageserver.boot
import java.io.File
import java.net.URI
import java.util.logging.ConsoleHandler
import akka.actor.ActorSystem
import org.enso.jsonrpc.JsonRpcServer
@ -29,8 +28,8 @@ import org.enso.languageserver.runtime._
import org.enso.languageserver.search.SuggestionsHandler
import org.enso.languageserver.session.SessionRouter
import org.enso.languageserver.text.BufferRegistry
import org.enso.languageserver.util.Logging
import org.enso.languageserver.util.binary.BinaryEncoder
import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel}
import org.enso.polyglot.{LanguageInfo, RuntimeOptions, RuntimeServerInfo}
import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo}
import org.enso.text.{ContentBasedVersioning, Sha3_224VersionCalculator}
@ -45,8 +44,9 @@ import scala.util.{Failure, Success}
/** A main module containing all components of the server.
*
* @param serverConfig configuration for the language server
* @param logLevel log level for the Language Server
*/
class MainModule(serverConfig: LanguageServerConfig) {
class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
val log = LoggerFactory.getLogger(this.getClass)
log.trace("Initializing...")
@ -150,21 +150,16 @@ class MainModule(serverConfig: LanguageServerConfig) {
val stdIn = new ObservablePipedInputStream(stdInSink)
log.trace("Initializing Runtime context...")
val logHandler = Logging.getLogHandler(LanguageInfo.ID) match {
case Left(t) =>
log.warn("Failed to create the Runtime logger", t)
new ConsoleHandler()
case Right(handler) =>
log.trace(s"Setting Runtime logger")
handler
}
val context = Context
.newBuilder(LanguageInfo.ID)
.allowAllAccess(true)
.allowExperimentalOptions(true)
.option(RuntimeServerInfo.ENABLE_OPTION, "true")
.option(RuntimeOptions.PACKAGES_PATH, serverConfig.contentRootPath)
.option(RuntimeOptions.LOG_LEVEL, logHandler.getLevel.toString)
.option(
RuntimeOptions.LOG_LEVEL,
JavaLoggingLogHandler.getJavaLogLevelFor(logLevel).getName
)
.option(
RuntimeServerInfo.JOB_PARALLELISM_OPTION,
Runtime.getRuntime.availableProcessors().toString
@ -172,7 +167,9 @@ class MainModule(serverConfig: LanguageServerConfig) {
.out(stdOut)
.err(stdErr)
.in(stdIn)
.logHandler(logHandler)
.logHandler(
JavaLoggingLogHandler.create(JavaLoggingLogHandler.defaultLevelMapping)
)
.serverTransport((uri: URI, peerEndpoint: MessageEndpoint) => {
if (uri.toString == RuntimeServerInfo.URI) {
val connection = new RuntimeConnector.Endpoint(
@ -187,8 +184,7 @@ class MainModule(serverConfig: LanguageServerConfig) {
context.initialize(LanguageInfo.ID)
log.trace("Runtime context initialized")
val logLevel = Logging.LogLevel.fromJava(logHandler.getLevel)
system.eventStream.setLogLevel(Logging.LogLevel.toAkka(logLevel))
system.eventStream.setLogLevel(LogLevel.toAkka(logLevel))
log.trace(s"Set akka log level to $logLevel")
val runtimeKiller =
@ -267,9 +263,18 @@ class MainModule(serverConfig: LanguageServerConfig) {
log.error("Failed to initialize SQL versions repo", ex)
}(system.dispatcher)
Future
val initialization = Future
.sequence(Seq(suggestionsRepoInit, versionsRepoInit))
.map(_ => ())
initialization.onComplete {
case Success(()) =>
system.eventStream.publish(InitializedEvent.InitializationFinished)
case _ =>
system.eventStream.publish(InitializedEvent.InitializationFailed)
}
initialization
}
/** Close the main module releasing all resources. */

View File

@ -7,4 +7,6 @@ object InitializedEvent {
case object SuggestionsRepoInitialized extends InitializedEvent
case object FileVersionsRepoInitialized extends InitializedEvent
case object InitializationFinished extends InitializedEvent
case object InitializationFailed extends InitializedEvent
}

View File

@ -17,4 +17,13 @@ object MonitoringApi {
}
}
case object InitialPing extends Method("heartbeat/init") {
implicit val hasParams = new HasParams[this.type] {
type Params = Unused.type
}
implicit val hasResult = new HasResult[this.type] {
type Result = Unused.type
}
}
}

View File

@ -21,12 +21,15 @@ import org.enso.languageserver.filemanager.PathWatcherProtocol
import org.enso.languageserver.io.InputOutputApi._
import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput}
import org.enso.languageserver.io.{InputOutputApi, InputOutputProtocol}
import org.enso.languageserver.monitoring.MonitoringApi.Ping
import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping}
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
import org.enso.languageserver.requesthandler._
import org.enso.languageserver.requesthandler.capability._
import org.enso.languageserver.requesthandler.io._
import org.enso.languageserver.requesthandler.monitoring.PingHandler
import org.enso.languageserver.requesthandler.monitoring.{
InitialPingHandler,
PingHandler
}
import org.enso.languageserver.requesthandler.refactoring.RenameProjectHandler
import org.enso.languageserver.requesthandler.session.InitProtocolConnectionHandler
import org.enso.languageserver.requesthandler.text._
@ -37,18 +40,12 @@ import org.enso.languageserver.requesthandler.visualisation.{
}
import org.enso.languageserver.runtime.ContextRegistryProtocol
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.search.SearchApi.{
Completion,
GetSuggestionsDatabase,
GetSuggestionsDatabaseVersion,
Import,
InvalidateSuggestionsDatabase
}
import org.enso.languageserver.runtime.VisualisationApi.{
AttachVisualisation,
DetachVisualisation,
ModifyVisualisation
}
import org.enso.languageserver.search.SearchApi._
import org.enso.languageserver.search.{SearchApi, SearchProtocol}
import org.enso.languageserver.session.JsonSession
import org.enso.languageserver.session.SessionApi.{
@ -246,6 +243,7 @@ class JsonConnectionController(
),
requestTimeout
),
InitialPing -> InitialPingHandler.props,
AcquireCapability -> AcquireCapabilityHandler
.props(capabilityRouter, requestTimeout, rpcSession),
ReleaseCapability -> ReleaseCapabilityHandler

View File

@ -10,7 +10,7 @@ import org.enso.languageserver.capability.CapabilityApi.{
}
import org.enso.languageserver.filemanager.FileManagerApi._
import org.enso.languageserver.io.InputOutputApi._
import org.enso.languageserver.monitoring.MonitoringApi.Ping
import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping}
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.search.SearchApi._
@ -24,6 +24,7 @@ object JsonRpc {
*/
val protocol: Protocol = Protocol.empty
.registerRequest(Ping)
.registerRequest(InitialPing)
.registerRequest(InitProtocolConnection)
.registerRequest(AcquireCapability)
.registerRequest(ReleaseCapability)

View File

@ -0,0 +1,57 @@
package org.enso.languageserver.requesthandler.monitoring
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult, Unused}
import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.monitoring.MonitoringApi
/** A request handler for `heartbeat/init` commands. */
class InitialPingHandler extends Actor with ActorLogging {
override def preStart(): Unit = {
context.system.eventStream.subscribe(self, classOf[InitializedEvent])
}
override def receive: Receive = waitingForInitialization(Nil)
private def waitingForInitialization(
pendingRequests: List[(ActorRef, Id)]
): Receive = {
case Request(MonitoringApi.InitialPing, id, Unused) =>
context.become(
waitingForInitialization((sender() -> id) :: pendingRequests)
)
case InitializedEvent.InitializationFinished =>
for ((ref, id) <- pendingRequests) {
ref ! ResponseResult(MonitoringApi.InitialPing, id, Unused)
}
context.become(initialized)
case InitializedEvent.InitializationFailed =>
for ((ref, id) <- pendingRequests) {
ref ! ResponseError(Some(id), ServiceError)
}
context.become(failed)
case _: InitializedEvent =>
}
private def initialized: Receive = {
case Request(MonitoringApi.InitialPing, id, Unused) =>
sender() ! ResponseResult(MonitoringApi.InitialPing, id, Unused)
}
private def failed: Receive = {
case Request(MonitoringApi.InitialPing, id, Unused) =>
ResponseError(Some(id), ServiceError)
}
}
object InitialPingHandler {
/** Creates a configuration object used to create a
* [[InitialPingHandler]]
*
* @return a configuration object
*/
def props: Props = Props(new InitialPingHandler)
}

View File

@ -1,41 +0,0 @@
package org.enso.languageserver.util
import java.util.logging.{Handler, Level, LogRecord}
import ch.qos.logback.classic
import org.slf4j.event
class LogHandler(logger: classic.Logger) extends Handler {
private val level = logger.getLevel
/** @inheritdoc */
override def publish(record: LogRecord): Unit = {
logger.log(
null,
record.getSourceClassName,
LogHandler.toSlf4j(record.getLevel).toInt,
record.getMessage,
record.getParameters,
record.getThrown
)
}
/** @inheritdoc */
override def flush(): Unit = ()
/** @inheritdoc */
override def close(): Unit = ()
/** @inheritdoc */
override def getLevel: Level =
Logging.LogLevel.toJava(Logging.LogLevel.fromLogback(level))
}
object LogHandler {
/** Convert java utils log level to slf4j. */
private def toSlf4j(level: Level): event.Level =
Logging.LogLevel.toSlf4j(Logging.LogLevel.fromJava(level))
}

View File

@ -1,134 +0,0 @@
package org.enso.languageserver.util
import java.util
import cats.syntax.either._
import ch.qos.logback.classic.{Level, Logger, LoggerContext}
import org.slf4j.{event, LoggerFactory}
object Logging {
/** Application log level. */
sealed trait LogLevel
object LogLevel {
case object Error extends LogLevel
case object Warning extends LogLevel
case object Info extends LogLevel
case object Debug extends LogLevel
case object Trace extends LogLevel
/** Convert to logback log level. */
def toLogback(level: LogLevel): Level =
level match {
case Error => Level.ERROR
case Warning => Level.WARN
case Info => Level.INFO
case Debug => Level.DEBUG
case Trace => Level.TRACE
}
/** Convert from logback log level. */
def fromLogback(level: Level): LogLevel = {
level match {
case Level.`ERROR` => Error
case Level.`WARN` => Warning
case Level.`INFO` => Info
case Level.`DEBUG` => Debug
case Level.`TRACE` => Trace
}
}
/** Convert to java util logging level. */
def toJava(level: LogLevel): util.logging.Level =
level match {
case Error => util.logging.Level.SEVERE
case Warning => util.logging.Level.WARNING
case Info => util.logging.Level.INFO
case Debug => util.logging.Level.FINE
case Trace => util.logging.Level.FINEST
}
/** Convert from java util logging level. */
def fromJava(level: util.logging.Level): LogLevel =
level match {
case util.logging.Level.`SEVERE` => LogLevel.Error
case util.logging.Level.`WARNING` => LogLevel.Warning
case util.logging.Level.`INFO` => LogLevel.Info
case util.logging.Level.`CONFIG` => LogLevel.Debug
case util.logging.Level.`FINE` => LogLevel.Debug
case util.logging.Level.`FINER` => LogLevel.Debug
case util.logging.Level.`FINEST` => LogLevel.Trace
}
/** Convert to slf4j logging level. */
def toSlf4j(level: LogLevel): event.Level =
level match {
case Error => event.Level.ERROR
case Warning => event.Level.WARN
case Info => event.Level.INFO
case Debug => event.Level.DEBUG
case Trace => event.Level.TRACE
}
/** Convert to akka logging level. */
def toAkka(level: LogLevel): akka.event.Logging.LogLevel =
level match {
case Error => akka.event.Logging.ErrorLevel
case Warning => akka.event.Logging.WarningLevel
case Info => akka.event.Logging.InfoLevel
case Debug => akka.event.Logging.DebugLevel
case Trace => akka.event.Logging.DebugLevel
}
}
private val ROOT_LOGGER = "org.enso"
/** Set log level for the application root logger.
*
* @param level the log level
* @return the new log level
*/
def setLogLevel(level: LogLevel): Either[Throwable, LogLevel] = {
Either.catchNonFatal {
val ctx = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
ctx.getLogger(ROOT_LOGGER).setLevel(LogLevel.toLogback(level))
level
}
}
/** Get log level of the application root logger. */
def getLogLevel: Either[Throwable, LogLevel] = {
Either.catchNonFatal {
val ctx = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
val level = ctx.getLogger(ROOT_LOGGER).getLevel
LogLevel.fromLogback(level)
}
}
/** Get the application logger.
*
* @param name the logger name
* @return the application logger
*/
def getLogger(name: String): Either[Throwable, Logger] = {
Either.catchNonFatal {
val ctx = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
ctx.getLogger(name)
}
}
/** Get the java log handler instance backed by the application logger.
*
* @param name the logger name
* @return the application log handler
*/
def getLogHandler(name: String): Either[Throwable, LogHandler] =
for {
level <- Logging.getLogLevel
logger <- Logging.getLogger(name)
} yield {
logger.setLevel(Logging.LogLevel.toLogback(level))
new LogHandler(logger)
}
}

View File

@ -1,18 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-15thread] %-5level %logger{36} %msg%n</pattern>
</encoder>
</appender>
<logger name="com.zaxxer.hikari" level="ERROR"/>
<logger name="slick" level="INFO"/>
<logger name="slick.compiler" level="INFO"/>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -29,7 +29,11 @@ class PingHandlerSpec
.props(List(subsystem1.ref, subsystem2.ref, subsystem3.ref), 10.seconds)
)
//when
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused)
actorUnderTest ! Request(
MonitoringApi.Ping,
Number(1),
Unused
)
//then
subsystem1.expectMsg(Ping)
subsystem2.expectMsg(Ping)
@ -48,7 +52,11 @@ class PingHandlerSpec
.props(List(subsystem1.ref, subsystem2.ref, subsystem3.ref), 10.seconds)
)
//when
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused)
actorUnderTest ! Request(
MonitoringApi.Ping,
Number(1),
Unused
)
subsystem1.expectMsg(Ping)
subsystem1.lastSender ! Pong
subsystem2.expectMsg(Ping)
@ -56,7 +64,9 @@ class PingHandlerSpec
subsystem3.expectMsg(Ping)
subsystem3.lastSender ! Pong
//then
expectMsg(ResponseResult(MonitoringApi.Ping, Number(1), Unused))
expectMsg(
ResponseResult(MonitoringApi.Ping, Number(1), Unused)
)
//teardown
system.stop(actorUnderTest)
}
@ -72,7 +82,11 @@ class PingHandlerSpec
)
watch(actorUnderTest)
//when
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused)
actorUnderTest ! Request(
MonitoringApi.Ping,
Number(1),
Unused
)
subsystem2.expectMsg(Ping)
subsystem2.lastSender ! Pong
subsystem3.expectMsg(Ping)

View File

@ -154,6 +154,7 @@ class BaseServerTest extends JsonRpcServerTestKit {
Await.ready(suggestionsRepoInit, timeout)
Await.ready(versionsRepoInit, timeout)
system.eventStream.publish(InitializedEvent.InitializationFinished)
new JsonConnectionControllerFactory(
bufferRegistry,

View File

@ -8,13 +8,13 @@ import org.enso.languageserver.search.Suggestions
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
import org.enso.polyglot.data.Tree
import org.enso.polyglot.runtime.Runtime.Api
import org.enso.testkit.FlakySpec
import org.enso.testkit.RetrySpec
class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
class SuggestionsHandlerEventsTest extends BaseServerTest with RetrySpec {
"SuggestionsHandlerEvents" must {
"send suggestions database notifications" taggedAs Flaky in {
"send suggestions database notifications" taggedAs Retry in {
val client = getInitialisedWsClient()
system.eventStream.publish(ProjectNameChangedEvent("Test", "Test"))

View File

@ -81,7 +81,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
.newProject(
path = actualPath,
name = name,
version = version,
engineVersion = version,
authorName = globalConfig.authorName,
authorEmail = globalConfig.authorEmail,
additionalArguments = additionalArguments
@ -414,7 +414,9 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
val (runtimeVersionRunSettings, whichEngine) = runner.version(useJSON).get
val isEngineInstalled =
componentsManager.findEngine(runtimeVersionRunSettings.version).isDefined
componentsManager
.findEngine(runtimeVersionRunSettings.engineVersion)
.isDefined
val runtimeVersionString = if (isEngineInstalled) {
val output = runner.withCommand(
runtimeVersionRunSettings,

View File

@ -120,14 +120,14 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
.newProject(
path = projectPath,
name = "ProjectName",
version = defaultEngineVersion,
engineVersion = defaultEngineVersion,
authorName = Some(authorName),
authorEmail = Some(authorEmail),
additionalArguments = Seq(additionalArgument)
)
.get
runSettings.version shouldEqual defaultEngineVersion
runSettings.engineVersion shouldEqual defaultEngineVersion
runSettings.runnerArguments should contain(additionalArgument)
val commandLine = runSettings.runnerArguments.mkString(" ")
commandLine should include(
@ -149,7 +149,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
runSettings.version shouldEqual defaultEngineVersion
runSettings.engineVersion shouldEqual defaultEngineVersion
runSettings.runnerArguments should (contain("arg") and contain("--flag"))
runSettings.runnerArguments.mkString(" ") should
(include("--repl") and not include s"--in-project")
@ -174,7 +174,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
outsideProject.version shouldEqual version
outsideProject.engineVersion shouldEqual version
outsideProject.runnerArguments.mkString(" ") should
(include(s"--in-project $normalizedPath") and include("--repl"))
@ -188,7 +188,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
insideProject.version shouldEqual version
insideProject.engineVersion shouldEqual version
insideProject.runnerArguments.mkString(" ") should
(include(s"--in-project $normalizedPath") and include("--repl"))
@ -202,7 +202,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
overriddenRun.version shouldEqual overridden
overriddenRun.engineVersion shouldEqual overridden
overriddenRun.runnerArguments.mkString(" ") should
(include(s"--in-project $normalizedPath") and include("--repl"))
}
@ -230,7 +230,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
runSettings.version shouldEqual version
runSettings.engineVersion shouldEqual version
val commandLine = runSettings.runnerArguments.mkString(" ")
commandLine should include(s"--interface ${options.interface}")
commandLine should include(s"--rpc-port ${options.rpcPort}")
@ -250,7 +250,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
logLevel = LogLevel.Info
)
.get
.version shouldEqual overridden
.engineVersion shouldEqual overridden
}
"run a project" in {
@ -270,7 +270,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
outsideProject.version shouldEqual version
outsideProject.engineVersion shouldEqual version
outsideProject.runnerArguments.mkString(" ") should
include(s"--run $normalizedPath")
@ -284,7 +284,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
insideProject.version shouldEqual version
insideProject.engineVersion shouldEqual version
insideProject.runnerArguments.mkString(" ") should
include(s"--run $normalizedPath")
@ -298,7 +298,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
overriddenRun.version shouldEqual overridden
overriddenRun.engineVersion shouldEqual overridden
overriddenRun.runnerArguments.mkString(" ") should
include(s"--run $normalizedPath")
@ -338,7 +338,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
runSettings.version shouldEqual defaultEngineVersion
runSettings.engineVersion shouldEqual defaultEngineVersion
runSettings.runnerArguments.mkString(" ") should
(include(s"--run $normalizedPath") and (not(include("--in-project"))))
}
@ -362,7 +362,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
)
.get
runSettings.version shouldEqual version
runSettings.engineVersion shouldEqual version
runSettings.runnerArguments.mkString(" ") should
(include(s"--run $normalizedFilePath") and
include(s"--in-project $normalizedProjectPath"))
@ -374,7 +374,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
.version(useJSON = true)
.get
runSettings.version shouldEqual defaultEngineVersion
runSettings.engineVersion shouldEqual defaultEngineVersion
runSettings.runnerArguments should
(contain("--version") and contain("--json"))
@ -391,7 +391,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
.version(useJSON = false)
.get
runSettings.version shouldEqual version
runSettings.engineVersion shouldEqual version
runSettings.runnerArguments should
(contain("--version") and not(contain("--json")))

View File

@ -72,7 +72,7 @@ class UpgradeSpec
*
* If `launcherVersion` is not provided, the default one is used.
*
* It waits a 100ms delay after creating the launcher copy to ensure that the
* It waits a 250ms delay after creating the launcher copy to ensure that the
* copy can be called right away after calling this function. It is not
* absolutely certain that this is helpful, but from time to time, the tests
* fail because the filesystem does not allow to access the executable as
@ -94,7 +94,7 @@ class UpgradeSpec
val root = launcherPath.getParent.getParent
FileSystem.writeTextFile(root / ".enso.portable", "mark")
}
Thread.sleep(100)
Thread.sleep(250)
}
/** Path to the launcher executable in the temporary distribution.
@ -156,7 +156,7 @@ class UpgradeSpec
}
"upgrade" should {
"upgrade to latest version (excluding broken)" in {
"upgrade to latest version (excluding broken)" taggedAs Retry in {
prepareDistribution(
portable = true,
launcherVersion = Some(SemVer(0, 0, 2))
@ -166,7 +166,7 @@ class UpgradeSpec
checkVersion() shouldEqual SemVer(0, 0, 4)
}
"not downgrade without being explicitly asked to do so" in {
"not downgrade without being explicitly asked to do so" taggedAs Retry in {
// precondition for the test to make sense
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
@ -177,7 +177,7 @@ class UpgradeSpec
}
"upgrade/downgrade to a specific version " +
"(and update necessary files)" in {
"(and update necessary files)" taggedAs Retry in {
// precondition for the test to make sense
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
@ -194,7 +194,7 @@ class UpgradeSpec
.trim shouldEqual "Test license"
}
"upgrade also in installed mode" in {
"upgrade also in installed mode" taggedAs Retry in {
prepareDistribution(
portable = false,
launcherVersion = Some(SemVer(0, 0, 0))
@ -228,37 +228,46 @@ class UpgradeSpec
)
checkVersion() shouldEqual SemVer(0, 0, 0)
run(Seq("upgrade", "0.0.3")) should returnSuccess
val process = startLauncher(Seq("upgrade", "0.0.3"))
try {
process.join(timeoutSeconds = 30) should returnSuccess
checkVersion() shouldEqual SemVer(0, 0, 3)
checkVersion() shouldEqual SemVer(0, 0, 3)
val launchedVersions = Seq(
"0.0.0",
"0.0.0",
"0.0.1",
"0.0.2",
"0.0.3"
)
val launchedVersions = Seq(
"0.0.0",
"0.0.0",
"0.0.1",
"0.0.2",
"0.0.3"
)
val reportedLaunchLog = TestHelpers
.readFileContent(launcherPath.getParent / ".launcher_version_log")
.trim
.linesIterator
.toSeq
val reportedLaunchLog = TestHelpers
.readFileContent(launcherPath.getParent / ".launcher_version_log")
.trim
.linesIterator
.toSeq
reportedLaunchLog shouldEqual launchedVersions
reportedLaunchLog shouldEqual launchedVersions
withClue(
"After the update we run the version check, running the launcher " +
"after the update should ensure no leftover temporary executables " +
"are left in the bin directory."
) {
val binDirectory = launcherPath.getParent
val leftOverExecutables = FileSystem
.listDirectory(binDirectory)
.map(_.getFileName.toString)
.filter(_.startsWith("enso"))
leftOverExecutables shouldEqual Seq(OS.executableName("enso"))
withClue(
"After the update we run the version check, running the launcher " +
"after the update should ensure no leftover temporary executables " +
"are left in the bin directory."
) {
val binDirectory = launcherPath.getParent
val leftOverExecutables = FileSystem
.listDirectory(binDirectory)
.map(_.getFileName.toString)
.filter(_.startsWith("enso"))
leftOverExecutables shouldEqual Seq(OS.executableName("enso"))
}
} finally {
if (process.isAlive) {
// ensure that the child process frees resources if retrying the test
process.kill()
Thread.sleep(500)
}
}
}
@ -271,8 +280,8 @@ class UpgradeSpec
val enginesPath = getTestDirectory / "enso" / "dist"
Files.createDirectories(enginesPath)
// TODO [RW] re-enable this test when #1046 is done and the engine
// distribution can be used in the test
// TODO [RW] re-enable this test when #1046 or #1273 is done and the
// engine distribution can be used in the test
// FileSystem.copyDirectory(
// Path.of("target/distribution/"),
// enginesPath / "0.1.0"

View File

@ -0,0 +1,31 @@
package org.enso.runner
import nl.gn0s1s.bump.SemVer
/** A helper object that allows to access current version of the runner.
*
* The current version is parsed from [[buildinfo]], but in development mode it
* can be overridden by setting `enso.version.override` property. This is used
* in project-manager tests to override the version of projects created using
* the runner.
*/
object CurrentVersion {
/** The version that the application should report. */
lazy val version: SemVer = computeVersion()
private def computeVersion(): SemVer = {
val buildVersion = SemVer(buildinfo.Info.ensoVersion).getOrElse {
throw new IllegalStateException(
"Fatal error: Enso version included in buildinfo is not a valid " +
"semver string, this should never happen."
)
}
if (buildinfo.Info.isRelease) buildVersion
else
sys.props
.get("enso.version.override")
.flatMap(SemVer(_))
.getOrElse(buildVersion)
}
}

View File

@ -4,6 +4,7 @@ import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import org.enso.loggingservice.LogLevel
import scala.concurrent.Await
import scala.concurrent.duration._
@ -16,10 +17,11 @@ object LanguageServerApp {
/** Runs a Language Server
*
* @param config a config
* @param logLevel log level
*/
def run(config: LanguageServerConfig): Unit = {
def run(config: LanguageServerConfig, logLevel: LogLevel): Unit = {
println("Starting Language Server...")
val server = new LanguageServerComponent(config)
val server = new LanguageServerComponent(config, logLevel)
Await.result(server.start(), 10.seconds)
StdIn.readLine()
Await.result(server.stop(), 10.seconds)

View File

@ -5,7 +5,6 @@ import java.util.UUID
import akka.http.scaladsl.model.{IllegalUriException, Uri}
import cats.implicits._
import nl.gn0s1s.bump.SemVer
import org.apache.commons.cli.{Option => CliOption, _}
import org.enso.languageserver.boot
import org.enso.languageserver.boot.LanguageServerConfig
@ -234,19 +233,13 @@ object Main {
): Unit = {
val root = new File(path)
val name = nameOption.getOrElse(PackageManager.Default.generateName(root))
val currentVersion = SemVer(buildinfo.Info.ensoVersion).getOrElse {
throw new IllegalStateException(
"Fatal error: Enso version included in buildinfo is not a valid " +
"semver string, this should never happen."
)
}
val authors =
if (authorName.isEmpty && authorEmail.isEmpty) List()
else List(Contact(name = authorName, email = authorEmail))
PackageManager.Default.create(
root = root,
name = name,
ensoVersion = SemVerEnsoVersion(currentVersion),
ensoVersion = SemVerEnsoVersion(CurrentVersion.version),
authors = authors,
maintainers = authors
)
@ -432,8 +425,6 @@ object Main {
* @param logLevel log level to set for the engine runtime
*/
private def runLanguageServer(line: CommandLine, logLevel: LogLevel): Unit = {
val _ = logLevel // TODO [RW] handle logging in the Language Server (#1144)
val maybeConfig = parseSeverOptions(line)
maybeConfig match {
@ -442,7 +433,7 @@ object Main {
exitFail()
case Right(config) =>
LanguageServerApp.run(config)
LanguageServerApp.run(config, logLevel)
exitSuccess()
}
}
@ -481,7 +472,8 @@ object Main {
def displayVersion(useJson: Boolean): Unit = {
val versionDescription = VersionDescription.make(
"Enso Compiler and Runtime",
includeRuntimeJVMInfo = true
includeRuntimeJVMInfo = true,
customVersion = Some(CurrentVersion.version.toString)
)
println(versionDescription.asString(useJson))
}

View File

@ -62,7 +62,7 @@ abstract class JsonRpcServerTestKit
val _ = binding.unbind()
}
class WsTestClient(address: String) {
class WsTestClient(address: String, debugMessages: Boolean = false) {
private var inActor: ActorRef = _
private val outActor: TestProbe = TestProbe()
private val source: Source[Message, NotUsed] = Source
@ -104,8 +104,11 @@ abstract class JsonRpcServerTestKit
def send(json: Json): Unit = send(json.noSpaces)
def expectMessage(timeout: FiniteDuration = 3.seconds.dilated): String =
outActor.expectMsgClass[String](timeout, classOf[String])
def expectMessage(timeout: FiniteDuration = 3.seconds.dilated): String = {
val message = outActor.expectMsgClass[String](timeout, classOf[String])
if (debugMessages) println(message)
message
}
def expectJson(
json: Json,

View File

@ -15,6 +15,7 @@ sealed abstract class LogLevel(final val level: Int) {
def shouldLog(other: LogLevel): Boolean =
other.level <= level
}
object LogLevel {
/** This log level should not be used by messages, instead it can be set as
@ -93,19 +94,38 @@ object LogLevel {
level.level.asJson
}
/** Creates a [[LogLevel]] from its integer representation.
*
* Returns None if the number does not represent a valid log level.
*/
def fromInteger(level: Int): Option[LogLevel] = level match {
case Error.level => Some(Error)
case Warning.level => Some(Warning)
case Info.level => Some(Info)
case Debug.level => Some(Debug)
case Trace.level => Some(Trace)
case _ => None
}
/** [[Decoder]] instance for [[LogLevel]].
*/
implicit val decoder: Decoder[LogLevel] = { json =>
json.as[Int].flatMap {
case Error.level => Right(Error)
case Warning.level => Right(Warning)
case Info.level => Right(Info)
case Debug.level => Right(Debug)
case Trace.level => Right(Trace)
case other =>
Left(
DecodingFailure(s"`$other` is not a valid log level.", json.history)
)
json.as[Int].flatMap { level =>
fromInteger(level).toRight(
DecodingFailure(s"`$level` is not a valid log level.", json.history)
)
}
}
/** Converts our internal [[LogLevel]] to the corresponding instance of
* Akka-specific log level.
*/
def toAkka(logLevel: LogLevel): akka.event.Logging.LogLevel = logLevel match {
case Off => akka.event.Logging.LogLevel(Int.MinValue)
case Error => akka.event.Logging.ErrorLevel
case Warning => akka.event.Logging.WarningLevel
case Info => akka.event.Logging.InfoLevel
case Debug => akka.event.Logging.DebugLevel
case Trace => akka.event.Logging.DebugLevel
}
}

View File

@ -1,12 +1,7 @@
package org.enso.loggingservice
import org.enso.loggingservice.internal.service.{Client, Local, Server, Service}
import org.enso.loggingservice.internal.{
BlockingConsumerMessageQueue,
InternalLogMessage,
InternalLogger,
LoggerConnection
}
import org.enso.loggingservice.internal._
import org.enso.loggingservice.printers.{Printer, StderrPrinter}
import scala.concurrent.{ExecutionContext, Future}
@ -14,8 +9,37 @@ import scala.concurrent.{ExecutionContext, Future}
/** Manages the logging service.
*/
object LoggingServiceManager {
private val messageQueue = new BlockingConsumerMessageQueue()
private var currentLevel: LogLevel = LogLevel.Trace
private val testLoggingPropertyKey = "org.enso.loggingservice.test-log-level"
private var currentService: Option[Service] = None
private var currentLevel: LogLevel = LogLevel.Trace
/** Creates an instance for the [[messageQueue]].
*
* Runs special workaround logic if test mode is detected.
*/
private def initializeMessageQueue(): BlockingConsumerMessageQueue = {
sys.props.get(testLoggingPropertyKey) match {
case Some(value) =>
val logLevel =
value.toIntOption.flatMap(LogLevel.fromInteger).getOrElse {
System.err.println(
s"Invalid log level for $testLoggingPropertyKey, " +
s"falling back to info."
)
LogLevel.Info
}
val shouldOverride = () => currentService.isEmpty
new TestMessageQueue(logLevel, shouldOverride)
case None => productionMessageQueue()
}
}
private def productionMessageQueue() = new BlockingConsumerMessageQueue()
private val messageQueue = initializeMessageQueue()
/** The default [[LoggerConnection]] that should be used by all backends which
* want to use the logging service.
@ -32,8 +56,6 @@ object LoggingServiceManager {
override def logLevel: LogLevel = currentLevel
}
private var currentService: Option[Service] = None
/** Sets up the logging service, but in a separate thread to avoid stalling
* the application.
*

View File

@ -0,0 +1,33 @@
package org.enso.loggingservice.internal
import org.enso.loggingservice.LogLevel
import org.enso.loggingservice.internal.protocol.WSLogMessage
import org.enso.loggingservice.printers.StderrPrinter
/** A message queue for use in testing.
*
* It has a smaller buffer and ignores messages from a certain log level.
*
* @param logLevel
* @param shouldOverride
*/
class TestMessageQueue(logLevel: LogLevel, shouldOverride: () => Boolean)
extends BlockingConsumerMessageQueue(bufferSize = 100) {
private def shouldKeepMessage(
message: Either[InternalLogMessage, WSLogMessage]
): Boolean = message match {
case Left(value) => logLevel.shouldLog(value.level)
case Right(value) => logLevel.shouldLog(value.level)
}
private val overridePrinter = StderrPrinter.create()
/** @inheritdoc */
override def send(message: Either[InternalLogMessage, WSLogMessage]): Unit =
if (shouldKeepMessage(message)) {
if (shouldOverride())
overridePrinter.print(message.fold(_.toLogMessage, identity))
else
super.send(message)
}
}

View File

@ -9,6 +9,7 @@ import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
import org.enso.projectmanager.control.effect.{Async, ErrorChannel, Exec, Sync}
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
import org.enso.projectmanager.infrastructure.languageserver.{
ExecutorWithUnlimitedPool,
LanguageServerGatewayImpl,
LanguageServerRegistry,
ShutdownHookActivator
@ -25,6 +26,7 @@ import org.enso.projectmanager.service.config.GlobalConfigService
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService
import org.enso.projectmanager.service.{
MonadicProjectValidator,
ProjectCreationService,
ProjectService,
ProjectServiceFailure,
ValidationFailure
@ -76,7 +78,9 @@ class MainModule[
config.network,
config.bootloader,
config.supervision,
config.timeout
config.timeout,
DefaultDistributionConfiguration,
ExecutorWithUnlimitedPool
),
"language-server-registry"
)
@ -94,19 +98,25 @@ class MainModule[
config.timeout
)
lazy val projectCreationService =
new ProjectCreationService[F](DefaultDistributionConfiguration)
lazy val globalConfigService =
new GlobalConfigService[F](DefaultDistributionConfiguration)
lazy val projectService =
new ProjectService[F](
projectValidator,
projectRepository,
projectCreationService,
globalConfigService,
logging,
clock,
gen,
languageServerGateway
languageServerGateway,
DefaultDistributionConfiguration
)
lazy val globalConfigService =
new GlobalConfigService[F](DefaultDistributionConfiguration)
lazy val runtimeVersionManagementService =
new RuntimeVersionManagementService[F](DefaultDistributionConfiguration)

View File

@ -8,7 +8,10 @@ import akka.pattern.pipe
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.stream.{CompletionStrategy, OverflowStrategy}
import org.enso.projectmanager.infrastructure.http.AkkaBasedWebSocketConnection._
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.Listen
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.{
Attach,
Detach
}
import org.enso.projectmanager.infrastructure.http.WebSocketConnection.{
WebSocketConnected,
WebSocketMessage,
@ -65,7 +68,11 @@ class AkkaBasedWebSocketConnection(address: String)(implicit
/** @inheritdoc */
override def attachListener(listener: ActorRef): Unit =
receiver ! Listen(listener)
receiver ! Attach(listener)
/** @inheritdoc */
override def detachListener(listener: ActorRef): Unit =
receiver ! Detach(listener)
/** @inheritdoc */
def connect(): Unit = {
@ -83,6 +90,7 @@ class AkkaBasedWebSocketConnection(address: String)(implicit
case InvalidUpgradeResponse(_, cause) =>
WebSocketStreamFailure(new Exception(s"Cannot connect $cause"))
}
.recover(WebSocketStreamFailure(_))
.pipeTo(receiver)
()
}

View File

@ -1,7 +1,10 @@
package org.enso.projectmanager.infrastructure.http
import akka.actor.{Actor, ActorRef}
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.Listen
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.{
Attach,
Detach
}
/** A fan-out receiver that delivers messages to multiple listeners.
*/
@ -10,7 +13,8 @@ class FanOutReceiver extends Actor {
override def receive: Receive = running()
private def running(listeners: Set[ActorRef] = Set.empty): Receive = {
case Listen(listener) => context.become(running(listeners + listener))
case Attach(listener) => context.become(running(listeners + listener))
case Detach(listener) => context.become(running(listeners - listener))
case msg => listeners.foreach(_ ! msg)
}
@ -22,6 +26,6 @@ object FanOutReceiver {
*
* @param listener a listener to attach
*/
case class Listen(listener: ActorRef)
case class Attach(listener: ActorRef)
case class Detach(listener: ActorRef)
}

View File

@ -26,6 +26,12 @@ trait WebSocketConnection {
*/
def attachListener(listener: ActorRef): Unit
/** Removes the listener of incoming messages.
*
* Can be useful when disconnecting has timed out and we do not want to
* receive the disconnected message after the owning actor is long dead.
*/
def detachListener(listener: ActorRef): Unit
}
object WebSocketConnection {

View File

@ -0,0 +1,134 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.io.PrintWriter
import java.lang.ProcessBuilder.Redirect
import java.util.concurrent.Executors
import akka.actor.ActorRef
import org.apache.commons.lang3.concurrent.BasicThreadFactory
import org.enso.loggingservice.LogLevel
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.runtimeversionmanager.runner.{LanguageServerOptions, Runner}
import scala.concurrent.Future
import scala.util.Using
object ExecutorWithUnlimitedPool extends LanguageServerExecutor {
/** An executor that ensures each job runs in a separate thread.
*
* It is used to run the process in a background thread. It is blocking by
* design to ensure that the locking API is used correctly. This executor
* should start no more than one thread per Language Server instance.
*/
private val forkedProcessExecutor = {
val threadFactory =
new BasicThreadFactory.Builder()
.namingPattern("language-server-pool-%d")
.build()
Executors.newCachedThreadPool(threadFactory)
}
/** @inheritdoc */
override def spawn(
descriptor: LanguageServerDescriptor,
progressTracker: ActorRef,
rpcPort: Int,
dataPort: Int,
lifecycleListener: LanguageServerExecutor.LifecycleListener
): Unit = {
val runnable: Runnable = { () =>
try {
runServer(
descriptor,
progressTracker,
rpcPort,
dataPort,
lifecycleListener
)
} catch {
case throwable: Throwable =>
lifecycleListener.onFailed(throwable)
}
}
forkedProcessExecutor.submit(runnable)
}
/** Runs the child process, ensuring that the proper locks are kept for the
* used engine for the whole lifetime of that process.
*
* Returns the exit code of the process. This function is blocking so it
* should be run in a backgroung thread.
*/
private def runServer(
descriptor: LanguageServerDescriptor,
progressTracker: ActorRef,
rpcPort: Int,
dataPort: Int,
lifecycleListener: LanguageServerExecutor.LifecycleListener
): Unit = {
val distributionConfiguration = descriptor.distributionConfiguration
val versionManager = RuntimeVersionManagerFactory(distributionConfiguration)
.makeRuntimeVersionManager(progressTracker)
// TODO [RW] logging #1151
val loggerConnection = Future.successful(None)
val logLevel = LogLevel.Info
val options = LanguageServerOptions(
rootId = descriptor.rootId,
interface = descriptor.networkConfig.interface,
rpcPort = rpcPort,
dataPort = dataPort
)
val runner = new Runner(
versionManager,
distributionConfiguration.environment,
loggerConnection
)
val runSettings = runner
.startLanguageServer(
options = options,
projectPath = descriptor.rootPath,
version = descriptor.engineVersion,
logLevel = logLevel,
additionalArguments = Seq()
)
.get
runner.withCommand(runSettings, descriptor.jvmSettings) { command =>
val process = {
val pb = command.builder()
pb.inheritIO()
pb.redirectInput(Redirect.PIPE)
if (descriptor.discardOutput) {
pb.redirectError(Redirect.DISCARD)
pb.redirectOutput(Redirect.DISCARD)
}
pb.start()
}
lifecycleListener.onStarted(new LanguageServerProcessHandle(process))
val exitCode = process.waitFor()
lifecycleListener.onTerminated(exitCode)
}
}
private class LanguageServerProcessHandle(private val process: Process)
extends LanguageServerExecutor.ProcessHandle {
/** Requests the child process to terminate gracefully by sending the
* termination request to its standard input stream.
*/
def requestGracefulTermination(): Unit =
Using(new PrintWriter(process.getOutputStream)) { writer =>
writer.println()
}
/** @inheritdoc */
def kill(): Unit = {
process.destroyForcibly()
}
}
}

View File

@ -17,7 +17,10 @@ import org.enso.projectmanager.infrastructure.languageserver.HeartbeatSession.{
HeartbeatTimeout,
SocketClosureTimeout
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerSupervisor.ServerUnresponsive
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerSupervisor.{
HeartbeatReceived,
ServerUnresponsive
}
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
@ -28,12 +31,21 @@ import scala.concurrent.duration.FiniteDuration
* @param timeout a session timeout
* @param connectionFactory a web socket connection factory
* @param scheduler a scheduler
* @param method api method to use for the heartbeat message
* @param sendConfirmations whether to send [[HeartbeatReceived]] to confirm
* that a response has been received
* @param quietErrors if set, reports errors in debug level instead of error
* level, can be used when errors are expected (i.e. on
* startup)
*/
class HeartbeatSession(
socket: Socket,
timeout: FiniteDuration,
connectionFactory: WebSocketConnectionFactory,
scheduler: Scheduler
scheduler: Scheduler,
method: String,
sendConfirmations: Boolean,
quietErrors: Boolean
) extends Actor
with ActorLogging
with UnhandledLogging {
@ -57,7 +69,7 @@ class HeartbeatSession(
connection.send(s"""
|{
| "jsonrpc": "2.0",
| "method": "heartbeat/ping",
| "method": "$method",
| "id": "$requestId",
| "params": null
|}
@ -66,7 +78,7 @@ class HeartbeatSession(
context.become(pongStage(cancellable))
case WebSocketStreamFailure(th) =>
log.error(th, s"An error occurred during connecting to websocket $socket")
logError(th, s"An error occurred during connecting to websocket $socket")
context.parent ! ServerUnresponsive
stop()
@ -81,16 +93,18 @@ class HeartbeatSession(
maybeJson match {
case Left(error) =>
log.error(error, "An error occurred during parsing pong reply")
logError(error, "An error occurred during parsing pong reply")
case Right(id) =>
if (id == requestId.toString) {
log.debug(s"Received correct pong message from $socket")
if (sendConfirmations) {
context.parent ! HeartbeatReceived
}
cancellable.cancel()
connection.disconnect()
val closureTimeout =
scheduler.scheduleOnce(timeout, self, SocketClosureTimeout)
context.become(socketClosureStage(closureTimeout))
stop()
} else {
log.warning(s"Received unknown response $payload")
}
@ -99,21 +113,17 @@ class HeartbeatSession(
case HeartbeatTimeout =>
log.debug(s"Heartbeat timeout detected for $requestId")
context.parent ! ServerUnresponsive
connection.disconnect()
val closureTimeout =
scheduler.scheduleOnce(timeout, self, SocketClosureTimeout)
context.become(socketClosureStage(closureTimeout))
stop()
case WebSocketStreamClosed =>
context.parent ! ServerUnresponsive
context.stop(self)
case WebSocketStreamFailure(th) =>
log.error(th, s"An error occurred during waiting for Pong message")
logError(th, s"An error occurred during waiting for Pong message")
context.parent ! ServerUnresponsive
cancellable.cancel()
connection.disconnect()
context.stop(self)
stop()
case GracefulStop =>
cancellable.cancel()
@ -126,13 +136,14 @@ class HeartbeatSession(
cancellable.cancel()
case WebSocketStreamFailure(th) =>
log.error(th, s"An error occurred during closing web socket")
logError(th, s"An error occurred during closing web socket")
context.stop(self)
cancellable.cancel()
case SocketClosureTimeout =>
log.error(s"Socket closure timed out")
logError(s"Socket closure timed out")
context.stop(self)
connection.detachListener(self)
case GracefulStop => // ignoring it, because the actor is already closing
}
@ -144,6 +155,22 @@ class HeartbeatSession(
context.become(socketClosureStage(closureTimeout))
}
private def logError(throwable: Throwable, message: String): Unit = {
if (quietErrors) {
log.debug(s"$message ($throwable)")
} else {
log.error(throwable, message)
}
}
private def logError(message: String): Unit = {
if (quietErrors) {
log.debug(message)
} else {
log.error(message)
}
}
}
object HeartbeatSession {
@ -156,7 +183,8 @@ object HeartbeatSession {
*/
case object SocketClosureTimeout
/** Creates a configuration object used to create a [[LanguageServerSupervisor]].
/** Creates a configuration object used to create an ordinary
* [[HeartbeatSession]] for monitoring server's status.
*
* @param socket a server socket
* @param timeout a session timeout
@ -170,6 +198,43 @@ object HeartbeatSession {
connectionFactory: WebSocketConnectionFactory,
scheduler: Scheduler
): Props =
Props(new HeartbeatSession(socket, timeout, connectionFactory, scheduler))
Props(
new HeartbeatSession(
socket,
timeout,
connectionFactory,
scheduler,
"heartbeat/ping",
sendConfirmations = false,
quietErrors = false
)
)
/** Creates a configuration object used to create an initial
* [[HeartbeatSession]] for checking if the server has finished booting.
*
* @param socket a server socket
* @param timeout a session timeout
* @param connectionFactory a web socket connection factory
* @param scheduler a scheduler
* @return a configuration object
*/
def initialProps(
socket: Socket,
timeout: FiniteDuration,
connectionFactory: WebSocketConnectionFactory,
scheduler: Scheduler
): Props =
Props(
new HeartbeatSession(
socket,
timeout,
connectionFactory,
scheduler,
"heartbeat/init",
sendConfirmations = true,
quietErrors = true
)
)
}

View File

@ -1,35 +1,52 @@
package org.enso.projectmanager.infrastructure.languageserver
import akka.actor.Status.Failure
import akka.actor.{Actor, ActorLogging, Props}
import akka.pattern.pipe
import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import org.enso.projectmanager.boot.configuration.BootloaderConfig
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{
Boot,
FindFreeSocket,
ServerBootFailed,
ServerBooted
}
import org.enso.projectmanager.infrastructure.net.Tcp
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/** It boots a Language Sever described by the `descriptor`. Upon boot failure
* looks up new available port and retries to boot the server.
*
* Once the server is booted it can restart it on request.
*
* The `bootProgressTracker` has to be provided because, while the process
* assumes that the required engine version has been pre-installed, it still
* may sometimes need to wait on a lock (for example if an engine is being
* uninstalled), so these events need to be reported to the progress tracker.
*
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
* related to initializing the engine
* @param descriptor a LS descriptor
* @param config a bootloader config
* @param bootTimeout a finite duration that determines the deadline for
* initialization of the server process, if the process
* starts but fails to initialize after this timeout, it is
* treated as a boot failure and the process is gracefully
* stopped
* @param executor an executor service used to start the language server
* process
*/
class LanguageServerBootLoader(
bootProgressTracker: ActorRef,
descriptor: LanguageServerDescriptor,
config: BootloaderConfig
config: BootloaderConfig,
bootTimeout: FiniteDuration,
executor: LanguageServerExecutor
) extends Actor
with ActorLogging
with UnhandledLogging {
// TODO [RW] consider adding a stop timeout so that if the graceful stop on
// timed-out boot also does not work, the process can be killed forcibly
// (#1315)
import context.dispatcher
override def preStart(): Unit = {
@ -39,6 +56,10 @@ class LanguageServerBootLoader(
override def receive: Receive = findingSocket()
/** First bootloader phase - looking for a free set of ports for the server.
*
* Once the ports are found, the process starts booting.
*/
private def findingSocket(retry: Int = 0): Receive = {
case FindFreeSocket =>
log.debug("Looking for available socket to bind the language server")
@ -53,53 +74,190 @@ class LanguageServerBootLoader(
s"binary:${descriptor.networkConfig.interface}:$binaryPort]"
)
self ! Boot
context.become(booting(jsonRpcPort, binaryPort, retry))
context.become(
bootingFirstTime(
rpcPort = jsonRpcPort,
dataPort = binaryPort,
retryCount = retry
)
)
case GracefulStop =>
context.stop(self)
}
private def booting(rpcPort: Int, dataPort: Int, retryCount: Int): Receive = {
/** This phase is triggered when the ports are found.
*
* When booting for the first time, the actor that will be notified is the
* parent and retries are allowed (upon retry new set of ports will be tried).
*/
private def bootingFirstTime(
rpcPort: Int,
dataPort: Int,
retryCount: Int
): Receive = booting(
rpcPort = rpcPort,
dataPort = dataPort,
shouldRetry = true,
retryCount = retryCount,
bootRequester = context.parent
)
/** A general booting phase.
*
* It spawns the [[LanguageServerProcess]] child actor and waits for it to
* confirm that the server has been successfully initialized (or failed to
* boot). Once booted it enters the running phase.
*/
private def booting(
rpcPort: Int,
dataPort: Int,
shouldRetry: Boolean,
retryCount: Int,
bootRequester: ActorRef
): Receive = {
case Boot =>
log.debug("Booting a language server")
val config = LanguageServerConfig(
descriptor.networkConfig.interface,
rpcPort,
dataPort,
descriptor.rootId,
descriptor.root,
descriptor.name,
context.dispatcher
context.actorOf(
LanguageServerProcess.props(
progressTracker = bootProgressTracker,
descriptor = descriptor,
bootTimeout = bootTimeout,
rpcPort = rpcPort,
dataPort = dataPort,
executor = executor
),
s"process-wrapper-${descriptor.name}"
)
val server = new LanguageServerComponent(config)
server.start().map(_ => config -> server) pipeTo self
case Failure(th) =>
log.error(
th,
s"An error occurred during boot of Language Server [${descriptor.name}]"
case LanguageServerProcess.ServerTerminated(exitCode) =>
handleBootFailure(
shouldRetry,
retryCount,
bootRequester,
s"Language server terminated with exit code $exitCode before " +
s"finishing booting.",
None
)
if (retryCount < config.numberOfRetries) {
context.system.scheduler
.scheduleOnce(config.delayBetweenRetry, self, FindFreeSocket)
context.become(findingSocket(retryCount + 1))
} else {
case LanguageServerProcess.ServerThreadFailed(throwable) =>
handleBootFailure(
shouldRetry,
retryCount,
bootRequester,
s"Language server thread failed with $throwable.",
Some(throwable)
)
case LanguageServerProcess.ServerConfirmedFinishedBooting =>
val connectionInfo = LanguageServerConnectionInfo(
descriptor.networkConfig.interface,
rpcPort = rpcPort,
dataPort = dataPort
)
log.info(s"Language server booted [$connectionInfo].")
bootRequester ! ServerBooted(connectionInfo, self)
context.become(running(connectionInfo))
case GracefulStop =>
context.children.foreach(_ ! LanguageServerProcess.Stop)
}
/** Handles a boot failure by logging it and depending on configuration,
* retrying or notifying the proper actor about the failure.
*/
private def handleBootFailure(
shouldRetry: Boolean,
retryCount: Int,
bootRequester: ActorRef,
message: String,
throwable: Option[Throwable]
): Unit = {
log.warning(message)
if (shouldRetry && retryCount < config.numberOfRetries) {
context.system.scheduler
.scheduleOnce(config.delayBetweenRetry, self, FindFreeSocket)
context.become(findingSocket(retryCount + 1))
} else {
if (shouldRetry) {
log.error(
s"Tried $retryCount times to boot Language Server. Giving up."
)
context.parent ! ServerBootFailed(th)
context.stop(self)
} else {
log.error("Failed to restart the server. Giving up.")
}
case (config: LanguageServerConfig, server: LanguageServerComponent) =>
log.info(s"Language server booted [$config].")
context.parent ! ServerBooted(config, server)
bootRequester ! ServerBootFailed(
throwable.getOrElse(new RuntimeException(message))
)
context.stop(self)
}
}
/** After successful boot, we cannot stop as it would stop our child process,
* so we just wait for it to terminate.
*
* The restart command can trigger the restarting phase which consists of two
* parts: waiting for the old process to shutdown and rebooting a new one.
*/
private def running(connectionInfo: LanguageServerConnectionInfo): Receive = {
case msg @ LanguageServerProcess.ServerTerminated(exitCode) =>
log.debug(
s"Language Server process has terminated with exit code $exitCode"
)
context.parent ! msg
context.stop(self)
case Restart =>
context.children.foreach(_ ! LanguageServerProcess.Stop)
context.become(
restartingWaitingForShutdown(connectionInfo, rebootRequester = sender())
)
case GracefulStop =>
context.stop(self)
context.children.foreach(_ ! LanguageServerProcess.Stop)
}
// TODO [RW] handling stop timeout (#1315)
// may also consider a stop timeout for GracefulStop and killing the process?
/** First phase of restart waits fot the old process to shutdown and boots the
* new process.
*/
def restartingWaitingForShutdown(
connectionInfo: LanguageServerConnectionInfo,
rebootRequester: ActorRef
): Receive = {
case LanguageServerProcess.ServerTerminated(exitCode) =>
log.debug(
s"Language Server process has terminated (as requested to reboot) " +
s"with exit code $exitCode"
)
context.become(rebooting(connectionInfo, rebootRequester))
self ! Boot
case GracefulStop =>
context.children.foreach(_ ! LanguageServerProcess.Stop)
}
/** Currently rebooting does not retry.
*
* We cannot directly re-use the retry logic from the initial boot, because
* we need to keep the ports unchanged, since they are already passed to
* other components that will try to connect there.
*/
def rebooting(
connectionInfo: LanguageServerConnectionInfo,
rebootRequester: ActorRef
): Receive = booting(
rpcPort = connectionInfo.rpcPort,
dataPort = connectionInfo.dataPort,
shouldRetry = false,
retryCount = config.numberOfRetries,
bootRequester = rebootRequester
)
private def findPort(): Int =
Tcp.findAvailablePort(
descriptor.networkConfig.interface,
@ -107,29 +265,41 @@ class LanguageServerBootLoader(
descriptor.networkConfig.maxPort
)
private case object FindFreeSocket
private case object Boot
}
object LanguageServerBootLoader {
/** Creates a configuration object used to create a [[LanguageServerBootLoader]].
*
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
* related to initializing the engine
* @param descriptor a LS descriptor
* @param config a bootloader config
* @param bootTimeout maximum time the server can use to boot,
* does not include the time needed to install any missing
* components
* @param executor an executor service used to start the language server
* process
* @return a configuration object
*/
def props(
bootProgressTracker: ActorRef,
descriptor: LanguageServerDescriptor,
config: BootloaderConfig
config: BootloaderConfig,
bootTimeout: FiniteDuration,
executor: LanguageServerExecutor
): Props =
Props(new LanguageServerBootLoader(descriptor, config))
/** Find free socket command.
*/
case object FindFreeSocket
/** Boot command.
*/
case object Boot
Props(
new LanguageServerBootLoader(
bootProgressTracker,
descriptor,
config,
bootTimeout,
executor: LanguageServerExecutor
)
)
/** Signals that server boot failed.
*
@ -139,12 +309,16 @@ object LanguageServerBootLoader {
/** Signals that server booted successfully.
*
* @param config a server config
* @param server a server lifecycle component
* @param connectionInfo a server config
* @param serverProcessManager an actor that manages the server process
* lifecycle, currently it is
* [[LanguageServerBootLoader]]
*/
case class ServerBooted(
config: LanguageServerConfig,
server: LanguageServerComponent
connectionInfo: LanguageServerConnectionInfo,
serverProcessManager: ActorRef
)
case class ServerTerminated(exitCode: Int)
}

View File

@ -0,0 +1,10 @@
package org.enso.projectmanager.infrastructure.languageserver
/** Describes how to connect to a Language Server instance by providing its
* interface and selected ports.
*/
case class LanguageServerConnectionInfo(
interface: String,
rpcPort: Int,
dataPort: Int
)

View File

@ -2,7 +2,6 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.Status.Failure
import akka.actor.{
Actor,
ActorLogging,
@ -14,12 +13,7 @@ import akka.actor.{
SupervisorStrategy,
Terminated
}
import akka.pattern.pipe
import org.enso.languageserver.boot.LifecycleComponent.ComponentStopped
import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.boot.configuration.{
BootloaderConfig,
NetworkConfig,
@ -34,35 +28,38 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootL
ServerBootFailed,
ServerBooted
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.{
Boot,
BootTimeout,
ServerDied,
ShutDownServer,
ShutdownTimeout
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
import org.enso.projectmanager.model.Project
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration._
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
/** A language server controller responsible for managing the server lifecycle.
* It delegates all tasks to other actors like bootloader or supervisor.
*
* @param project a project open by the server
* @param engineVersion engine version to use for the language server
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
* related to initializing the engine
* @param networkConfig a net config
* @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config
* @param timeoutConfig a timeout config
* @param distributionConfiguration configuration of the distribution
* @param executor an executor service used to start the language server
* process
*/
class LanguageServerController(
project: Project,
engineVersion: SemVer,
bootProgressTracker: ActorRef,
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
) extends Actor
with ActorLogging
with Stash
@ -72,10 +69,14 @@ class LanguageServerController(
private val descriptor =
LanguageServerDescriptor(
name = s"language-server-${project.id}",
rootId = UUID.randomUUID(),
root = project.path.get,
networkConfig = networkConfig
name = s"language-server-${project.id}",
rootId = UUID.randomUUID(),
rootPath = project.path.get,
networkConfig = networkConfig,
distributionConfiguration = distributionConfiguration,
engineVersion = engineVersion,
jvmSettings = distributionConfiguration.defaultJVMSettings,
discardOutput = distributionConfiguration.shouldDiscardChildOutput
)
override def supervisorStrategy: SupervisorStrategy =
@ -92,21 +93,23 @@ class LanguageServerController(
case Boot =>
val bootloader =
context.actorOf(
LanguageServerBootLoader.props(descriptor, bootloaderConfig),
"bootloader"
LanguageServerBootLoader
.props(
bootProgressTracker,
descriptor,
bootloaderConfig,
timeoutConfig.bootTimeout,
executor
),
s"bootloader-${descriptor.name}"
)
context.watch(bootloader)
val timeoutCancellable =
context.system.scheduler.scheduleOnce(30.seconds, self, BootTimeout)
context.become(booting(bootloader, timeoutCancellable))
context.become(booting(bootloader))
case _ => stash()
}
private def booting(
Bootloader: ActorRef,
timeoutCancellable: Cancellable
): Receive = {
private def booting(Bootloader: ActorRef): Receive = {
case BootTimeout =>
log.error(s"Booting failed for $descriptor")
unstashAll()
@ -115,28 +118,25 @@ class LanguageServerController(
case ServerBootFailed(th) =>
log.error(th, s"Booting failed for $descriptor")
unstashAll()
timeoutCancellable.cancel()
context.become(bootFailed(LanguageServerProtocol.ServerBootFailed(th)))
case ServerBooted(config, server) =>
case ServerBooted(connectionInfo, serverProcessManager) =>
unstashAll()
timeoutCancellable.cancel()
context.become(supervising(config, server))
context.become(supervising(connectionInfo, serverProcessManager))
context.actorOf(
LanguageServerSupervisor.props(
config,
server,
connectionInfo,
serverProcessManager,
supervisionConfig,
new AkkaBasedWebSocketConnectionFactory(),
context.system.scheduler
),
"supervisor"
s"supervisor-${descriptor.name}"
)
case Terminated(Bootloader) =>
log.error(s"Bootloader for project ${project.name} failed")
unstashAll()
timeoutCancellable.cancel()
context.become(
bootFailed(
LanguageServerProtocol.ServerBootFailed(
@ -149,33 +149,57 @@ class LanguageServerController(
}
private def supervising(
config: LanguageServerConfig,
server: LanguageServerComponent,
connectionInfo: LanguageServerConnectionInfo,
serverProcessManager: ActorRef,
clients: Set[UUID] = Set.empty
): Receive = {
case StartServer(clientId, _) =>
sender() ! ServerStarted(
LanguageServerSockets(
Socket(config.interface, config.rpcPort),
Socket(config.interface, config.dataPort)
case StartServer(clientId, _, requestedEngineVersion, _) =>
if (requestedEngineVersion != engineVersion) {
sender() ! ServerBootFailed(
new IllegalStateException(
s"Requested to boot a server version $requestedEngineVersion, " +
s"but a server for this project with a different version, " +
s"$engineVersion, is already running. Two servers with different " +
s"versions cannot be running for a single project."
)
)
)
context.become(supervising(config, server, clients + clientId))
} else {
sender() ! ServerStarted(
LanguageServerSockets(
Socket(connectionInfo.interface, connectionInfo.rpcPort),
Socket(connectionInfo.interface, connectionInfo.dataPort)
)
)
context.become(
supervising(connectionInfo, serverProcessManager, clients + clientId)
)
}
case Terminated(_) =>
log.debug(s"Bootloader for $project terminated.")
case StopServer(clientId, _) =>
removeClient(config, server, clients, clientId, Some(sender()))
removeClient(
connectionInfo,
serverProcessManager,
clients,
clientId,
Some(sender())
)
case ShutDownServer =>
shutDownServer(server, None)
shutDownServer(None)
case ClientDisconnected(clientId) =>
removeClient(config, server, clients, clientId, None)
removeClient(
connectionInfo,
serverProcessManager,
clients,
clientId,
None
)
case RenameProject(_, oldName, newName) =>
val socket = Socket(config.interface, config.rpcPort)
val socket = Socket(connectionInfo.interface, connectionInfo.rpcPort)
context.actorOf(
ProjectRenameAction
.props(
@ -191,33 +215,31 @@ class LanguageServerController(
)
case ServerDied =>
log.error(s"Language server died [$config]")
log.error(s"Language server died [$connectionInfo]")
context.stop(self)
}
private def removeClient(
config: LanguageServerConfig,
server: LanguageServerComponent,
connectionInfo: LanguageServerConnectionInfo,
serverProcessManager: ActorRef,
clients: Set[UUID],
clientId: UUID,
maybeRequester: Option[ActorRef]
): Unit = {
val updatedClients = clients - clientId
if (updatedClients.isEmpty) {
shutDownServer(server, maybeRequester)
shutDownServer(maybeRequester)
} else {
sender() ! CannotDisconnectOtherClients
context.become(supervising(config, server, updatedClients))
context.become(
supervising(connectionInfo, serverProcessManager, updatedClients)
)
}
}
private def shutDownServer(
server: LanguageServerComponent,
maybeRequester: Option[ActorRef]
): Unit = {
private def shutDownServer(maybeRequester: Option[ActorRef]): Unit = {
log.debug(s"Shutting down a language server for project ${project.id}")
context.children.foreach(_ ! GracefulStop)
server.stop() pipeTo self
val cancellable =
context.system.scheduler
.scheduleOnce(timeoutConfig.shutdownTimeout, self, ShutdownTimeout)
@ -225,7 +247,7 @@ class LanguageServerController(
}
private def bootFailed(failure: ServerStartupFailure): Receive = {
case StartServer(_, _) =>
case StartServer(_, _, _, _) =>
sender() ! failure
stop()
}
@ -234,18 +256,16 @@ class LanguageServerController(
cancellable: Cancellable,
maybeRequester: Option[ActorRef]
): Receive = {
case Failure(th) =>
case LanguageServerProcess.ServerTerminated(exitCode) =>
cancellable.cancel()
log.error(
th,
s"An error occurred during Language server shutdown [$project]."
)
maybeRequester.foreach(_ ! FailureDuringShutdown(th))
stop()
case ComponentStopped =>
cancellable.cancel()
log.info(s"Language server shut down successfully [$project].")
if (exitCode == 0) {
log.info(s"Language server shut down successfully [$project].")
} else {
log.warning(
s"Language server shut down with non-zero exit code: $exitCode " +
s"[$project]."
)
}
maybeRequester.foreach(_ ! ServerStopped)
stop()
@ -254,7 +274,7 @@ class LanguageServerController(
maybeRequester.foreach(_ ! ServerShutdownTimedOut)
stop()
case StartServer(_, _) =>
case StartServer(_, _, _, _) =>
sender() ! PreviousInstanceNotShutDown
}
@ -283,26 +303,40 @@ object LanguageServerController {
/** Creates a configuration object used to create a [[LanguageServerController]].
*
* @param project a project open by the server
* @param engineVersion engine version to use for the language server
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
* related to initializing the engine
* @param networkConfig a net config
* @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config
* @param timeoutConfig a timeout config
* @param distributionConfiguration configuration of the distribution
* @param executor an executor service used to start the language server
* process
* @return a configuration object
*/
def props(
project: Project,
engineVersion: SemVer,
bootProgressTracker: ActorRef,
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
): Props =
Props(
new LanguageServerController(
project,
engineVersion,
bootProgressTracker,
networkConfig,
bootloaderConfig,
supervisionConfig,
timeoutConfig
timeoutConfig,
distributionConfiguration,
executor
)
)

View File

@ -2,18 +2,32 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.boot.configuration.NetworkConfig
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.runner.JVMSettings
/** A descriptor used to start up a Language Server.
/** A descriptor specifying options related to starting a Language Server.
*
* @param name a name of the LS
* @param rootId a content root id
* @param root a path to the content root
* @param rootPath a path to the content root
* @param networkConfig a network config
* @param distributionConfiguration configuration of current distribution, used
* to find installed (or install new) engine
* versions
* @param engineVersion version of the langauge server's engine to use
* @param jvmSettings settings to use for the JVM that will host the engine
* @param discardOutput specifies if the process output should be discarded or
* printed to parent's streams
*/
case class LanguageServerDescriptor(
name: String,
rootId: UUID,
root: String,
networkConfig: NetworkConfig
rootPath: String,
networkConfig: NetworkConfig,
distributionConfiguration: DistributionConfiguration,
engineVersion: SemVer,
jvmSettings: JVMSettings,
discardOutput: Boolean
)

View File

@ -0,0 +1,60 @@
package org.enso.projectmanager.infrastructure.languageserver
import akka.actor.ActorRef
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerExecutor.LifecycleListener
/** A service responsible for executing the forked language server process. */
trait LanguageServerExecutor {
/** Starts the language server process in a background thread.
*
* @param descriptor options related to this language server instance
* @param progressTracker reference to an actor that should be notifed of any
* locks
* @param rpcPort port to use for the RPC channel
* @param dataPort port to use for the binary channel
* @param lifecycleListener a listener that will be notified when the process
* is started and terminated
*/
def spawn(
descriptor: LanguageServerDescriptor,
progressTracker: ActorRef,
rpcPort: Int,
dataPort: Int,
lifecycleListener: LifecycleListener
): Unit
}
object LanguageServerExecutor {
/** Listens for lifecycle updates of a language server process. */
trait LifecycleListener {
/** Called when the process has been successfully started.
*
* @param processHandle a handle that can be used to terminate the process
*/
def onStarted(processHandle: ProcessHandle): Unit
/** Called when the process has terminated (either on request or abruptly).
*
* @param exitCode exit code of the child process
*/
def onTerminated(exitCode: Int): Unit
/** Called when the process fails to start or its execution terminates with
* an unexpected exception.
*/
def onFailed(throwable: Throwable): Unit
}
/** A handle to the running Language Server child process. */
trait ProcessHandle {
/** Requests the child process to terminate gracefully. */
def requestGracefulTermination(): Unit
/** Tries to forcibly kill the process. */
def kill(): Unit
}
}

View File

@ -2,6 +2,8 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.LanguageServerSockets
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol.{
CheckTimeout,
@ -20,13 +22,20 @@ trait LanguageServerGateway[F[+_, +_]] {
/** Starts a language server.
*
* It assumes that the required engine version has been preinstalled.
*
* @param progressTracker an ActorRef that should get notifications when
* waiting on a lock
* @param clientId a requester id
* @param project a project to start
* @param version engine version to use for the launched language server
* @return either a failure or sockets that a language server listens on
*/
def start(
progressTracker: ActorRef,
clientId: UUID,
project: Project
project: Project,
version: SemVer
): F[ServerStartupFailure, LanguageServerSockets]
/** Stops a lang. server.

View File

@ -5,6 +5,7 @@ import java.util.UUID
import akka.actor.{ActorRef, ActorSystem}
import akka.pattern.ask
import akka.util.Timeout
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
@ -38,14 +39,23 @@ class LanguageServerGatewayImpl[
/** @inheritdoc */
override def start(
progressTracker: ActorRef,
clientId: UUID,
project: Project
project: Project,
version: SemVer
): F[ServerStartupFailure, LanguageServerSockets] = {
implicit val timeout: Timeout = Timeout(timeoutConfig.bootTimeout)
implicit val timeout: Timeout = Timeout(2 * timeoutConfig.bootTimeout)
// TODO [RW] this can timeout if the boot is stuck waiting on a lock, how do
// we want to handle that? #1315
Async[F]
.fromFuture { () =>
(registry ? StartServer(clientId, project)).mapTo[ServerStartupResult]
(registry ? StartServer(
clientId,
project,
version,
progressTracker
)).mapTo[ServerStartupResult]
}
.mapError(_ => ServerBootTimedOut)
.flatMap {

View File

@ -0,0 +1,235 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.{Actor, ActorRef, Cancellable, Props, Stash}
import org.enso.projectmanager.data.Socket
import org.enso.projectmanager.infrastructure.http.AkkaBasedWebSocketConnectionFactory
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerExecutor.ProcessHandle
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProcess.{
Kill,
ServerTerminated,
ServerThreadFailed,
Stop
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerSupervisor.{
HeartbeatReceived,
ServerUnresponsive
}
import scala.concurrent.duration.{DurationInt, FiniteDuration}
/** An Actor that manages a single Language Server process.
*
* It starts the process upon creation and notifies the parent once the process
* has finished booting.
*
* @param progressTracker an [[ActorRef]] that will get progress updates
* related to initializing the engine
* @param descriptor a LS descriptor
* @param rpcPort port to bind for RPC connections
* @param dataPort port to bind for binary connections
* @param bootTimeout maximum time permitted to wait for the process to finish
* initializing; if the initialization heartbeat is not
* received within this time the boot is treated as failed
* and the process is gracefully stopped
* @param executor an executor service used to start the language server
* process
*/
class LanguageServerProcess(
progressTracker: ActorRef,
descriptor: LanguageServerDescriptor,
rpcPort: Int,
dataPort: Int,
bootTimeout: FiniteDuration,
executor: LanguageServerExecutor
) extends Actor
with Stash {
import context.dispatcher
override def preStart(): Unit = {
super.preStart()
self ! Boot
}
override def receive: Receive = initializationStage
object LifecycleListener extends LanguageServerExecutor.LifecycleListener {
override def onStarted(processHandle: ProcessHandle): Unit =
self ! ProcessStarted(processHandle)
override def onTerminated(exitCode: Int): Unit =
self ! ProcessTerminated(exitCode)
override def onFailed(throwable: Throwable): Unit =
self ! ProcessFailed(throwable)
}
/** First stage, it launches the child process and sets up the futures to send
* back the notifications and goes to the startingStage.
*/
private def initializationStage: Receive = {
case Boot =>
executor.spawn(
descriptor = descriptor,
progressTracker = progressTracker,
rpcPort = rpcPort,
dataPort = dataPort,
lifecycleListener = LifecycleListener
)
context.become(startingStage)
case _ => stash()
}
/** Waits for the process to actually start (this may take a long time if
* there are locked locks).
*
* Once the process is started, it goes to the bootingStage.
*/
private def startingStage: Receive = {
case ProcessStarted(process) =>
val cancellable =
context.system.scheduler.scheduleOnce(bootTimeout, self, TimedOut)
context.become(bootingStage(process, cancellable))
unstashAll()
self ! AskServerIfStarted
case ProcessFailed(error) => handleFatalError(error)
case _ => stash()
}
private def handleFatalError(error: Throwable): Unit = {
context.parent ! ServerThreadFailed(error)
context.stop(self)
}
/** In booting stage, the actor is retrying to connect to the server to verify
* that it has finished initialization.
*
* Once initialization is confirmed, it notifies the parent and proceeds to
* runningStage.
*
* Before initialization is confirmed the actor can also:
* - trigger the timeout or receive a graceful stop message which will ask
* the child process to terminate gracefully,
* - get a notification that the child process has terminated spuriously
* (possibly a crash).
*/
private def bootingStage(
process: ProcessHandle,
bootTimeout: Cancellable
): Receive =
handleBootResponse(process, bootTimeout).orElse(runningStage(process))
case object AskServerIfStarted
case object TimedOut
private val retryDelay = 100.milliseconds
private def handleBootResponse(
process: ProcessHandle,
bootTimeout: Cancellable
): Receive = {
case AskServerIfStarted =>
val socket = Socket(descriptor.networkConfig.interface, rpcPort)
context.actorOf(
HeartbeatSession.initialProps(
socket,
retryDelay,
new AkkaBasedWebSocketConnectionFactory()(context.system),
context.system.scheduler
),
s"initial-heartbeat-${UUID.randomUUID()}"
)
case TimedOut =>
self ! Stop
case HeartbeatReceived =>
context.parent ! LanguageServerProcess.ServerConfirmedFinishedBooting
bootTimeout.cancel()
context.become(runningStage(process))
case ServerUnresponsive =>
import context.dispatcher
context.system.scheduler.scheduleOnce(
retryDelay,
self,
AskServerIfStarted
)
context.become(bootingStage(process, bootTimeout))
}
/** When the process is running, the actor is monitoring it and managing its
* lifetime.
*
* If termination is requested, the process will be asked to gracefully
* terminate or be killed. If the process terminates (either on request or
* spuriously, e.g. by a crash), the parent is notified and the current actor
* is stopped.
*/
private def runningStage(process: ProcessHandle): Receive = {
case Stop => process.requestGracefulTermination()
case Kill => process.kill()
case ProcessTerminated(exitCode) =>
context.parent ! ServerTerminated(exitCode)
context.stop(self)
case ProcessFailed(error) => handleFatalError(error)
}
private case object Boot
private case class ProcessStarted(process: ProcessHandle)
private case class ProcessTerminated(exitCode: Int)
private case class ProcessFailed(throwable: Throwable)
}
object LanguageServerProcess {
/** Creates a configuration object used to create a [[LanguageServerProcess]].
*
* @param progressTracker an [[ActorRef]] that will get progress updates
* related to initializing the engine
* @param descriptor a LS descriptor
* @param rpcPort port to bind for RPC connections
* @param dataPort port to bind for binary connections
* @param bootTimeout maximum time permitted to wait for the process to finish
* initializing; if the initialization heartbeat is not
* received within this time the boot is treated as failed
* and the process is gracefully stopped
* @param executor an executor service used to start the language server
* process
* @return a configuration object
*/
def props(
progressTracker: ActorRef,
descriptor: LanguageServerDescriptor,
rpcPort: Int,
dataPort: Int,
bootTimeout: FiniteDuration,
executor: LanguageServerExecutor
): Props = Props(
new LanguageServerProcess(
progressTracker,
descriptor,
rpcPort,
dataPort,
bootTimeout,
executor
)
)
/** Sent to the parent when the server has terminated (for any reason: on
* request or on its own, e.g. due to a signal or crash).
*/
case class ServerTerminated(exitCode: Int)
/** Sent to the parent when starting the server has failed with an exception.
*/
case class ServerThreadFailed(throwable: Throwable)
/** Sent to the parent when the child process has confirmed that it is fully
* initialized.
*/
case object ServerConfirmedFinishedBooting
/** Sent to forcibly kill the server. */
case object Kill
/** Sent to gracefully request to stop the server. */
case object Stop
}

View File

@ -2,6 +2,8 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.LanguageServerSockets
import org.enso.projectmanager.model.Project
@ -13,8 +15,16 @@ object LanguageServerProtocol {
*
* @param clientId the requester id
* @param project the project to start
* @param engineVersion version of the engine to use
* @param progressTracker an actor that should be sent notifications about
* locks
*/
case class StartServer(clientId: UUID, project: Project)
case class StartServer(
clientId: UUID,
project: Project,
engineVersion: SemVer,
progressTracker: ActorRef
)
/** Base trait for server startup results.
*/

View File

@ -20,21 +20,27 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProto
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
import org.enso.projectmanager.util.UnhandledLogging
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
/** An actor that routes request regarding lang. server lifecycle to the
* right controller that manages the server.
* It creates a controller actor, if a server doesn't exists.
* It creates a controller actor, if a server doesn't exist.
*
* @param networkConfig a net config
* @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config
* @param timeoutConfig a timeout config
* @param distributionConfiguration configuration of the distribution
* @param executor an executor service used to start the language server
* process
*/
class LanguageServerRegistry(
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
) extends Actor
with ActorLogging
with UnhandledLogging {
@ -44,7 +50,7 @@ class LanguageServerRegistry(
private def running(
serverControllers: Map[UUID, ActorRef] = Map.empty
): Receive = {
case msg @ StartServer(_, project) =>
case msg @ StartServer(_, project, engineVersion, progressTracker) =>
if (serverControllers.contains(project.id)) {
serverControllers(project.id).forward(msg)
} else {
@ -52,10 +58,14 @@ class LanguageServerRegistry(
LanguageServerController
.props(
project,
engineVersion,
progressTracker,
networkConfig,
bootloaderConfig,
supervisionConfig,
timeoutConfig
timeoutConfig,
distributionConfiguration,
executor
),
s"language-server-controller-${project.id}"
)
@ -116,20 +126,27 @@ object LanguageServerRegistry {
* @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config
* @param timeoutConfig a timeout config
* @return
* @param distributionConfiguration configuration of the distribution
* @param executor an executor service used to start the language server
* process
* @return a configuration object
*/
def props(
networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig
timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
): Props =
Props(
new LanguageServerRegistry(
networkConfig,
bootloaderConfig,
supervisionConfig,
timeoutConfig
timeoutConfig,
distributionConfiguration,
executor
)
)

View File

@ -2,24 +2,24 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.Status.Failure
import akka.actor.{
Actor,
ActorLogging,
ActorRef,
Cancellable,
Props,
Scheduler,
Terminated
}
import akka.pattern.pipe
import org.enso.languageserver.boot.LifecycleComponent.ComponentRestarted
import org.enso.languageserver.boot.{LanguageServerConfig, LifecycleComponent}
import org.enso.projectmanager.boot.configuration.SupervisionConfig
import org.enso.projectmanager.data.Socket
import org.enso.projectmanager.infrastructure.http.WebSocketConnectionFactory
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{
ServerBootFailed,
ServerBooted
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.ServerDied
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerSupervisor.{
RestartServer,
SendHeartbeat,
ServerUnresponsive,
StartSupervision
@ -30,15 +30,16 @@ import org.enso.projectmanager.util.UnhandledLogging
* restarting it when the server is unresponsive. It delegates server
* monitoring to the [[HeartbeatSession]] actor.
*
* @param config a server config
* @param server a server handle
* @param connectionInfo a server connection info
* @param serverProcessManager an actor that manages the lifecycle of the
* server process
* @param supervisionConfig a supervision config
* @param connectionFactory a web socket connection factory
* @param scheduler a scheduler
*/
class LanguageServerSupervisor(
config: LanguageServerConfig,
server: LifecycleComponent,
connectionInfo: LanguageServerConnectionInfo,
serverProcessManager: ActorRef,
supervisionConfig: SupervisionConfig,
connectionFactory: WebSocketConnectionFactory,
scheduler: Scheduler
@ -69,7 +70,7 @@ class LanguageServerSupervisor(
private def supervising(cancellable: Cancellable): Receive = {
case SendHeartbeat =>
val socket = Socket(config.interface, config.rpcPort)
val socket = Socket(connectionInfo.interface, connectionInfo.rpcPort)
context.actorOf(
HeartbeatSession.props(
socket,
@ -81,40 +82,31 @@ class LanguageServerSupervisor(
)
case ServerUnresponsive =>
log.info(s"Server is unresponsive [$config]. Restarting it...")
log.info(s"Server is unresponsive [$connectionInfo]. Restarting it...")
cancellable.cancel()
log.info(s"Restarting first time the server")
server.restart() pipeTo self
context.become(restarting())
log.info(s"Restarting the server")
serverProcessManager ! Restart
context.become(restarting)
case GracefulStop =>
cancellable.cancel()
stop()
}
private def restarting(restartCount: Int = 1): Receive = {
case RestartServer =>
log.info(s"Restarting $restartCount time the server")
server.restart() pipeTo self
()
private def restarting: Receive = {
case ServerBootFailed(_) =>
log.error("Cannot restart language server")
context.parent ! ServerDied
context.stop(self)
case Failure(th) =>
log.error(th, s"An error occurred during restarting the server [$config]")
if (restartCount < supervisionConfig.numberOfRestarts) {
scheduler.scheduleOnce(
supervisionConfig.delayBetweenRestarts,
self,
RestartServer
case ServerBooted(_, newProcessManager) =>
if (newProcessManager != serverProcessManager) {
log.error(
"The process manager actor has changed. This should never happen. " +
"Supervisor may no longer work correctly."
)
context.become(restarting(restartCount + 1))
} else {
log.error("Cannot restart language server")
context.parent ! ServerDied
context.stop(self)
}
case ComponentRestarted =>
log.info(s"Language server restarted [$config]")
log.info(s"Language server restarted [$connectionInfo]")
val cancellable =
scheduler.scheduleAtFixedRate(
supervisionConfig.initialDelay,
@ -150,8 +142,6 @@ object LanguageServerSupervisor {
private case object StartSupervision
private case object RestartServer
/** A command responsible for initiating heartbeat session.
*/
case object SendHeartbeat
@ -160,26 +150,30 @@ object LanguageServerSupervisor {
*/
case object ServerUnresponsive
/** Signals that the heartbeat has been received (only sent if demanded). */
case object HeartbeatReceived
/** Creates a configuration object used to create a [[LanguageServerSupervisor]].
*
* @param config a server config
* @param server a server handle
* @param connectionInfo a server config
* @param serverProcessManager an actor that manages the lifecycle of the
* server process
* @param supervisionConfig a supervision config
* @param connectionFactory a web socket connection factory
* @param scheduler a scheduler
* @return a configuration object
*/
def props(
config: LanguageServerConfig,
server: LifecycleComponent,
connectionInfo: LanguageServerConnectionInfo,
serverProcessManager: ActorRef,
supervisionConfig: SupervisionConfig,
connectionFactory: WebSocketConnectionFactory,
scheduler: Scheduler
): Props =
Props(
new LanguageServerSupervisor(
config,
server,
connectionInfo,
serverProcessManager,
supervisionConfig,
connectionFactory,
scheduler

View File

@ -2,8 +2,9 @@ package org.enso.projectmanager.infrastructure
package object languageserver {
/** A stop command.
*/
/** A stop command. */
case object GracefulStop
/** Requests to restart the language server. */
case object Restart
}

View File

@ -1,6 +1,7 @@
package org.enso.projectmanager.infrastructure.repository
import java.io.File
import java.nio.file.Path
import java.util.UUID
import org.enso.pkg.{Package, PackageManager}
@ -73,16 +74,9 @@ class ProjectFileRepository[
getAll().map(_.find(_.id == projectId))
/** @inheritdoc */
override def create(
override def findPathForNewProject(
project: Project
): F[ProjectRepositoryFailure, Unit] =
for {
projectPath <- findTargetPath(project)
_ <- createProjectStructure(project, projectPath)
_ <- metadataStorage(projectPath)
.persist(ProjectMetadata(project))
.mapError(th => StorageFailure(th.toString))
} yield ()
): F[ProjectRepositoryFailure, Path] = findTargetPath(project).map(_.toPath)
private def tryLoadProject(
directory: File
@ -97,12 +91,13 @@ class ProjectFileRepository[
meta <- metaOpt
} yield {
Project(
id = meta.id,
name = pkg.name,
kind = meta.kind,
created = meta.created,
lastOpened = meta.lastOpened,
path = Some(directory.toString)
id = meta.id,
name = pkg.name,
kind = meta.kind,
created = meta.created,
engineVersion = pkg.config.ensoVersion,
lastOpened = meta.lastOpened,
path = Some(directory.toString)
)
}
}
@ -115,14 +110,6 @@ class ProjectFileRepository[
.map(Some(_))
.mapError(_.fold(convertFileStorageFailure))
private def createProjectStructure(
project: Project,
projectPath: File
): F[StorageFailure, Package[File]] =
Sync[F]
.blockingOp { PackageManager.Default.create(projectPath, project.name) }
.mapError(th => StorageFailure(th.toString))
/** @inheritdoc */
override def rename(
projectId: UUID,

View File

@ -1,6 +1,7 @@
package org.enso.projectmanager.infrastructure.repository
import java.io.File
import java.nio.file.Path
import java.util.UUID
import org.enso.projectmanager.model.Project
@ -18,12 +19,15 @@ trait ProjectRepository[F[+_, +_]] {
*/
def exists(name: String): F[ProjectRepositoryFailure, Boolean]
/** Creates the provided user project in the storage.
/** Ensures that the path property is set in the project.
*
* @param project the project to insert
* @return
* If it was not set, a new path is generated for it. Otherwise, the function
* acts as identity.
*
* @param project the project to find the path for
* @return the project, with the updated path
*/
def create(project: Project): F[ProjectRepositoryFailure, Unit]
def findPathForNewProject(project: Project): F[ProjectRepositoryFailure, Path]
/** Saves the provided user project in the index.
*

View File

@ -3,12 +3,15 @@ package org.enso.projectmanager.model
import java.time.OffsetDateTime
import java.util.UUID
import org.enso.pkg.{DefaultEnsoVersion, EnsoVersion}
/** Project entity.
*
* @param id a project id
* @param name a project name
* @param kind a project kind
* @param created a project creation time
* @param engineVersion version of the engine associated with the project
* @param lastOpened a project last open time
* @param path a path to the project structure
*/
@ -17,6 +20,7 @@ case class Project(
name: String,
kind: ProjectKind,
created: OffsetDateTime,
engineVersion: EnsoVersion = DefaultEnsoVersion,
lastOpened: Option[OffsetDateTime] = None,
path: Option[String] = None
)

View File

@ -6,7 +6,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
import org.enso.jsonrpc.{JsonRpcServer, MessageHandler, Method, Request}
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec}
import org.enso.projectmanager.event.ClientEvent.{
ClientConnected,
ClientDisconnected
@ -30,7 +30,7 @@ import scala.concurrent.duration._
* @param runtimeVersionManagementService version management service
* @param timeoutConfig a request timeout config
*/
class ClientController[F[+_, +_]: Exec: CovariantFlatMap](
class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
clientId: UUID,
projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F],
@ -44,11 +44,19 @@ class ClientController[F[+_, +_]: Exec: CovariantFlatMap](
private val requestHandlers: Map[Method, Props] =
Map(
ProjectCreate -> ProjectCreateHandler
.props[F](projectService, timeoutConfig.requestTimeout),
.props[F](
globalConfigService,
projectService,
timeoutConfig.requestTimeout
),
ProjectDelete -> ProjectDeleteHandler
.props[F](projectService, timeoutConfig.requestTimeout),
ProjectOpen -> ProjectOpenHandler
.props[F](clientId, projectService, timeoutConfig.bootTimeout),
.props[F](
clientId,
projectService,
timeoutConfig.bootTimeout
),
ProjectClose -> ProjectCloseHandler
.props[F](
clientId,
@ -118,7 +126,7 @@ object ClientController {
* @param timeoutConfig a request timeout config
* @return a configuration object
*/
def props[F[+_, +_]: Exec: CovariantFlatMap](
def props[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
clientId: UUID,
projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F],

View File

@ -6,7 +6,7 @@ import akka.actor.{ActorRef, ActorSystem}
import org.enso.jsonrpc.ClientControllerFactory
import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec}
import org.enso.projectmanager.service.ProjectServiceApi
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementServiceApi
@ -19,7 +19,9 @@ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagemen
* @param runtimeVersionManagementService version management service
* @param timeoutConfig a request timeout config
*/
class ManagerClientControllerFactory[F[+_, +_]: Exec: CovariantFlatMap](
class ManagerClientControllerFactory[
F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel
](
system: ActorSystem,
projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F],

View File

@ -321,4 +321,8 @@ object ProjectManagementApi {
case class GlobalConfigurationAccessError(msg: String)
extends Error(4011, msg)
case class ProjectCreateError(msg: String) extends Error(4012, msg)
case class LoggingServiceUnavailable(msg: String) extends Error(4013, msg)
}

View File

@ -26,12 +26,11 @@ class EngineInstallHandler[F[+_, +_]: Exec: CovariantFlatMap](
/** @inheritdoc */
override def handleRequest = { params =>
val progressTracker = sender()
for {
_ <- service.installEngine(
progressTracker,
params.version,
params.forceInstallBroken.getOrElse(false)
progressTracker = self,
version = params.version,
forceInstallBroken = params.forceInstallBroken.getOrElse(false)
)
} yield Unused
}

View File

@ -29,9 +29,11 @@ class EngineUninstallHandler[F[+_, +_]: Exec: CovariantFlatMap](
/** @inheritdoc */
override def handleRequest = { params =>
val progressTracker = sender()
for {
_ <- service.uninstallEngine(progressTracker, params.version)
_ <- service.uninstallEngine(
progressTracker = self,
version = params.version
)
} yield Unused
}
}

View File

@ -1,109 +1,86 @@
package org.enso.projectmanager.requesthandler
import java.util.UUID
import akka.actor._
import akka.pattern.pipe
import org.enso.jsonrpc.Errors.{NotImplementedError, ServiceError}
import org.enso.jsonrpc._
import org.enso.pkg.DefaultEnsoVersion
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec}
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectCreate
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.mapFailure
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.failureMapper
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
import org.enso.projectmanager.service.{
ProjectServiceApi,
ProjectServiceFailure
}
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/** A request handler for `project/create` commands.
*
* @param service a project service
* @param configurationService the configuration service
* @param projectService a project service
* @param requestTimeout a request timeout
*/
class ProjectCreateHandler[F[+_, +_]: Exec](
service: ProjectServiceApi[F],
class ProjectCreateHandler[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
configurationService: GlobalConfigServiceApi[F],
projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration
) extends Actor
with ActorLogging
with UnhandledLogging {
override def receive: Receive = requestStage
) extends RequestHandler[
F,
ProjectServiceFailure,
ProjectCreate.type,
ProjectCreate.Params,
ProjectCreate.Result
](
ProjectCreate,
Some(requestTimeout)
) {
import context.dispatcher
override def handleRequest = { params =>
val version = params.version.getOrElse(DefaultEnsoVersion)
val missingComponentAction =
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
private def requestStage: Receive = {
case Request(ProjectCreate, id, params: ProjectCreate.Params) =>
if (params.version.isDefined) {
// TODO [RW] just to indicate that choosing specific version is not yet
// implemented, should be removed once that functionality is added
sender() ! ResponseError(Some(id), NotImplementedError)
context.stop(self)
} else {
val version = params.version.getOrElse(DefaultEnsoVersion)
val missingComponentAction =
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
Exec[F]
.exec(
service
.createUserProject(params.name, version, missingComponentAction)
for {
actualVersion <- configurationService
.resolveEnsoVersion(version)
.mapError { error =>
ProjectServiceFailure.ComponentRepositoryAccessFailure(
s"Could not determine the default version: $error"
)
.pipeTo(self)
val cancellable =
context.system.scheduler
.scheduleOnce(requestTimeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(ex, s"Failure during $ProjectCreate operation:")
replyTo ! ResponseError(Some(id), ServiceError)
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $ProjectCreate with $id timed out")
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case Left(failure: ProjectServiceFailure) =>
log.error(s"Request $id failed due to $failure")
replyTo ! ResponseError(Some(id), mapFailure(failure))
cancellable.cancel()
context.stop(self)
case Right(projectId: UUID) =>
replyTo ! ResponseResult(
ProjectCreate,
id,
ProjectCreate.Result(projectId)
}
projectId <- projectService.createUserProject(
progressTracker = self,
name = params.name,
engineVersion = actualVersion,
missingComponentAction = missingComponentAction
)
cancellable.cancel()
context.stop(self)
} yield ProjectCreate.Result(projectId)
}
}
object ProjectCreateHandler {
/** Creates a configuration object used to create a [[ProjectCreateHandler]].
*
* @param service a project service
* @param configurationService
* @param projectService a project service
* @param requestTimeout a request timeout
* @return a configuration object
*/
def props[F[+_, +_]: Exec](
service: ProjectServiceApi[F],
def props[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
configurationService: GlobalConfigServiceApi[F],
projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration
): Props =
Props(new ProjectCreateHandler(service, requestTimeout))
Props(
new ProjectCreateHandler(
configurationService,
projectService,
requestTimeout
)
)
}

View File

@ -2,88 +2,59 @@ package org.enso.projectmanager.requesthandler
import java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
import akka.pattern.pipe
import org.enso.jsonrpc.Errors.ServiceError
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import akka.actor.Props
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.data.{
LanguageServerSockets,
MissingComponentAction
}
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectOpen
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.mapFailure
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.failureMapper
import org.enso.projectmanager.service.{
ProjectServiceApi,
ProjectServiceFailure
}
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/** A request handler for `project/open` commands.
*
* @param clientId the requester id
* @param service a project service
* @param projectService a project service
* @param requestTimeout a request timeout
*/
class ProjectOpenHandler[F[+_, +_]: Exec](
class ProjectOpenHandler[F[+_, +_]: Exec: CovariantFlatMap](
clientId: UUID,
service: ProjectServiceApi[F],
projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration
) extends Actor
with ActorLogging
with UnhandledLogging {
override def receive: Receive = requestStage
) extends RequestHandler[
F,
ProjectServiceFailure,
ProjectOpen.type,
ProjectOpen.Params,
ProjectOpen.Result
](
ProjectOpen,
// TODO [RW] maybe we can get rid of this timeout since boot timeout is
// handled by the LanguageServerProcess; still the ? message of
// LanguageServerGateway will result in timeouts
Some(requestTimeout)
) {
import context.dispatcher
override def handleRequest = { params =>
val missingComponentAction =
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
private def requestStage: Receive = {
case Request(ProjectOpen, id, params: ProjectOpen.Params) =>
val missingComponentAction =
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
Exec[F]
.exec(
service
.openProject(clientId, params.projectId, missingComponentAction)
)
.pipeTo(self)
val cancellable =
context.system.scheduler
.scheduleOnce(requestTimeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case Status.Failure(ex) =>
log.error(ex, s"Failure during $ProjectOpen operation:")
replyTo ! ResponseError(Some(id), ServiceError)
cancellable.cancel()
context.stop(self)
case RequestTimeout =>
log.error(s"Request $ProjectOpen with $id timed out")
replyTo ! ResponseError(Some(id), ServiceError)
context.stop(self)
case Left(failure: ProjectServiceFailure) =>
log.error(s"Request $id failed due to $failure")
replyTo ! ResponseError(Some(id), mapFailure(failure))
cancellable.cancel()
context.stop(self)
case Right(sockets: LanguageServerSockets) =>
replyTo ! ResponseResult(
ProjectOpen,
id,
ProjectOpen.Result(sockets.jsonSocket, sockets.binarySocket)
for {
sockets <- projectService.openProject(
progressTracker = self,
clientId = clientId,
projectId = params.projectId,
missingComponentAction = missingComponentAction
)
cancellable.cancel()
context.stop(self)
} yield ProjectOpen.Result(
languageServerJsonAddress = sockets.jsonSocket,
languageServerBinaryAddress = sockets.binarySocket
)
}
}
@ -93,15 +64,21 @@ object ProjectOpenHandler {
/** Creates a configuration object used to create a [[ProjectOpenHandler]].
*
* @param clientId the requester id
* @param service a project service
* @param projectService a project service
* @param requestTimeout a request timeout
* @return a configuration object
*/
def props[F[+_, +_]: Exec](
def props[F[+_, +_]: Exec: CovariantFlatMap](
clientId: UUID,
service: ProjectServiceApi[F],
projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration
): Props =
Props(new ProjectOpenHandler(clientId, service, requestTimeout))
Props(
new ProjectOpenHandler(
clientId,
projectService,
requestTimeout
)
)
}

View File

@ -19,6 +19,7 @@ object ProjectServiceFailureMapper {
case DataStoreFailure(msg) => ProjectDataStoreError(msg)
case ProjectExists => ProjectExistsError
case ProjectNotFound => ProjectNotFoundError
case ProjectCreateFailed(msg) => ProjectCreateError(msg)
case ProjectOpenFailed(msg) => ProjectOpenError(msg)
case ProjectCloseFailed(msg) => ProjectCloseError(msg)
case ProjectNotOpen => ProjectNotOpenError

View File

@ -61,7 +61,7 @@ abstract class RequestHandler[
.exec(result)
.map(_.map(ResponseResult(method, request.id, _)))
.pipeTo(self)
val cancellable = {
val timeoutCancellable = {
requestTimeout.map { timeout =>
context.system.scheduler.scheduleOnce(
timeout,
@ -70,7 +70,7 @@ abstract class RequestHandler[
)
}
}
context.become(responseStage(request.id, sender(), cancellable))
context.become(responseStage(request.id, sender(), timeoutCancellable))
}
/** Defines the actual logic for handling the request.
@ -86,12 +86,12 @@ abstract class RequestHandler[
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Option[Cancellable]
timeoutCancellable: Option[Cancellable]
): Receive = {
case Status.Failure(ex) =>
log.error(ex, s"Failure during $method operation:")
replyTo ! ResponseError(Some(id), ServiceError)
cancellable.foreach(_.cancel())
timeoutCancellable.foreach(_.cancel())
context.stop(self)
case RequestTimeout =>
@ -103,15 +103,34 @@ abstract class RequestHandler[
log.error(s"Request $id failed due to $failure")
val error = implicitly[FailureMapper[FailureType]].mapFailure(failure)
replyTo ! ResponseError(Some(id), error)
cancellable.foreach(_.cancel())
timeoutCancellable.foreach(_.cancel())
context.stop(self)
case Right(response) =>
replyTo ! response
cancellable.foreach(_.cancel())
timeoutCancellable.foreach(_.cancel())
context.stop(self)
case notification: ProgressNotification =>
notification match {
case ProgressNotification.TaskStarted(_, _, _) =>
abandonTimeout(id, replyTo, timeoutCancellable)
case _ =>
}
replyTo ! translateProgressNotification(method.name, notification)
}
/** Cancels the timeout operation.
*
* Should be called when a long-running task is detected that we do not want
* to interrupt.
*/
private def abandonTimeout(
id: Id,
replyTo: ActorRef,
timeoutCancellable: Option[Cancellable]
): Unit = {
timeoutCancellable.foreach(_.cancel())
context.become(responseStage(id, replyTo, None))
}
}

View File

@ -0,0 +1,68 @@
package org.enso.projectmanager.service
import java.nio.file.Path
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.projectmanager.service.ProjectServiceFailure.ProjectCreateFailed
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.runner.Runner
import scala.concurrent.Future
/** A service for creating new project structures using the runner of the
* specific engine version selected for the project.
*/
class ProjectCreationService[
F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap
](
distributionConfiguration: DistributionConfiguration
) extends ProjectCreationServiceApi[F] {
/** @inheritdoc */
override def createProject(
progressTracker: ActorRef,
path: Path,
name: String,
engineVersion: SemVer,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, Unit] = Sync[F]
.blockingOp {
val versionManager = RuntimeVersionManagerFactory(
distributionConfiguration
).makeRuntimeVersionManager(progressTracker, missingComponentAction)
val runner =
new Runner(
versionManager,
distributionConfiguration.environment,
Future.successful(None)
)
val settings =
runner.newProject(path, name, engineVersion, None, None, Seq()).get
val jvmSettings = distributionConfiguration.defaultJVMSettings
runner.withCommand(settings, jvmSettings) { command =>
command.run().get
}
}
.mapRuntimeManagerErrors { other: Throwable =>
ProjectCreateFailed(other.getMessage)
}
.flatMap { exitCode =>
if (exitCode == 0)
CovariantFlatMap[F].pure(())
else
ErrorChannel[F].fail(
ProjectCreateFailed(
s"The runner used to create the project returned exit code " +
s"$exitCode."
)
)
}
}

View File

@ -0,0 +1,30 @@
package org.enso.projectmanager.service
import java.nio.file.Path
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.MissingComponentAction
/** An abstraction for creating new project structures under the given location.
*/
trait ProjectCreationServiceApi[F[+_, +_]] {
/** Creates a project with the provided configuration.
*
* @param progressTracker an actor that will be sent notifcation regarding
* progress of installation of any missing components
* or waiting on locks
* @param path path at which to create the project
* @param name name of the project
* @param engineVersion version of the engine this project is meant for
* @param missingComponentAction specifies how to handle missing components
*/
def createProject(
progressTracker: ActorRef,
path: Path,
name: String,
engineVersion: SemVer,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, Unit]
}

View File

@ -2,19 +2,21 @@ package org.enso.projectmanager.service
import java.util.UUID
import akka.actor.ActorRef
import cats.MonadError
import org.enso.pkg.{EnsoVersion, PackageManager}
import nl.gn0s1s.bump.SemVer
import org.enso.pkg.PackageManager
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.data.{
LanguageServerSockets,
MissingComponentAction,
ProjectMetadata
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerGateway
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.log.Logging
import org.enso.projectmanager.infrastructure.random.Generator
import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailure.{
@ -35,11 +37,18 @@ import org.enso.projectmanager.service.ValidationFailure.{
EmptyName,
NameContainsForbiddenCharacter
}
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
/** Implementation of business logic for project management.
*
* @param validator a project validator
* @param repo a project repository
* @param projectCreationService a service for creating projects
* @param configurationService a service for managing configuration
* @param log a logging facility
* @param clock a clock
* @param gen a random generator
@ -47,10 +56,13 @@ import org.enso.projectmanager.service.ValidationFailure.{
class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
validator: ProjectValidator[F],
repo: ProjectRepository[F],
projectCreationService: ProjectCreationServiceApi[F],
configurationService: GlobalConfigServiceApi[F],
log: Logging[F],
clock: Clock[F],
gen: Generator[F],
languageServerGateway: LanguageServerGateway[F]
languageServerGateway: LanguageServerGateway[F],
distributionConfiguration: DistributionConfiguration
)(implicit E: MonadError[F[ProjectServiceFailure, *], ProjectServiceFailure])
extends ProjectServiceApi[F] {
@ -58,25 +70,30 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
/** @inheritdoc */
override def createUserProject(
progressTracker: ActorRef,
name: String,
version: EnsoVersion,
engineVersion: SemVer,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, UUID] = {
// TODO [RW] new component handling
val _ = (version, missingComponentAction)
// format: off
for {
projectId <- gen.randomUUID()
_ <- log.debug(s"Creating project $name $projectId.")
_ <- validateName(name)
_ <- checkIfNameExists(name)
creationTime <- clock.nowInUtc()
project = Project(projectId, name, UserProject, creationTime)
_ <- repo.create(project).mapError(toServiceFailure)
_ <- log.info(s"Project $project created.")
} yield projectId
// format: on
}
): F[ProjectServiceFailure, UUID] = for {
projectId <- gen.randomUUID()
_ <- log.debug(s"Creating project $name $projectId.")
_ <- validateName(name)
_ <- checkIfNameExists(name)
creationTime <- clock.nowInUtc()
project = Project(projectId, name, UserProject, creationTime)
path <- repo.findPathForNewProject(project).mapError(toServiceFailure)
_ <- projectCreationService.createProject(
progressTracker,
path,
name,
engineVersion,
missingComponentAction
)
_ <- repo
.update(project.copy(path = Some(path.toString)))
.mapError(toServiceFailure)
_ <- log.info(s"Project $project created.")
} yield projectId
/** @inheritdoc */
override def deleteUserProject(
@ -176,12 +193,11 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
/** @inheritdoc */
override def openProject(
progressTracker: ActorRef,
clientId: UUID,
projectId: UUID,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, LanguageServerSockets] = {
// TODO [RW] new component handling
val _ = missingComponentAction
// format: off
for {
_ <- log.debug(s"Opening project $projectId")
@ -189,17 +205,46 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
openTime <- clock.nowInUtc()
updated = project.copy(lastOpened = Some(openTime))
_ <- repo.update(updated).mapError(toServiceFailure)
sockets <- startServer(clientId, updated)
sockets <- startServer(progressTracker, clientId, updated, missingComponentAction)
} yield sockets
// format: on
}
private def preinstallEngine(
progressTracker: ActorRef,
version: SemVer,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, Unit] =
Sync[F]
.blockingOp {
RuntimeVersionManagerFactory(distributionConfiguration)
.makeRuntimeVersionManager(progressTracker, missingComponentAction)
.findOrInstallEngine(version)
()
}
.mapRuntimeManagerErrors(th =>
ProjectOpenFailed(
s"Cannot install the required engine ${th.getMessage}"
)
)
private def startServer(
progressTracker: ActorRef,
clientId: UUID,
project: Project
): F[ProjectServiceFailure, LanguageServerSockets] =
languageServerGateway
.start(clientId, project)
project: Project,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, LanguageServerSockets] = for {
version <- configurationService
.resolveEnsoVersion(project.engineVersion)
.mapError { case ConfigurationFileAccessFailure(message) =>
ProjectOpenFailed(
s"Could not deduce the default version to use for the project: " +
s"$message"
)
}
_ <- preinstallEngine(progressTracker, version, missingComponentAction)
sockets <- languageServerGateway
.start(progressTracker, clientId, project, version)
.mapError {
case PreviousInstanceNotShutDown =>
ProjectOpenFailed(
@ -215,6 +260,7 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
s"Language server boot failed: ${th.getMessage}"
)
}
} yield sockets
/** @inheritdoc */
override def closeProject(

View File

@ -2,7 +2,8 @@ package org.enso.projectmanager.service
import java.util.UUID
import org.enso.pkg.EnsoVersion
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.{
LanguageServerSockets,
MissingComponentAction,
@ -17,14 +18,16 @@ trait ProjectServiceApi[F[+_, +_]] {
/** Creates a user project.
*
* @param progressTracker the actor to send progress updates to
* @param name the name of th project
* @param version Enso version to use for the new project
* @param engineVersion Enso version to use for the new project
* @param missingComponentAction specifies how to handle missing components
* @return projectId
*/
def createUserProject(
progressTracker: ActorRef,
name: String,
version: EnsoVersion,
engineVersion: SemVer,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, UUID]
@ -48,11 +51,14 @@ trait ProjectServiceApi[F[+_, +_]] {
/** Opens a project. It starts up a Language Server if needed.
*
* @param progressTracker the actor to send progress updates to
* @param clientId the requester id
* @param projectId the project id
* @param missingComponentAction specifies how to handle missing components
* @return either failure or a socket of the Language Server
*/
def openProject(
progressTracker: ActorRef,
clientId: UUID,
projectId: UUID,
missingComponentAction: MissingComponentAction

View File

@ -28,6 +28,12 @@ object ProjectServiceFailure {
*/
case object ProjectNotFound extends ProjectServiceFailure
/** Signals that a failure occured when creating the project.
*
* @param message a failure message
*/
case class ProjectCreateFailed(message: String) extends ProjectServiceFailure
/** Signals that a failure occurred during project startup.
*
* @param message a failure message

View File

@ -1,6 +1,9 @@
package org.enso.projectmanager.service.config
import io.circe.Json
import nl.gn0s1s.bump.SemVer
import org.enso.pkg.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion}
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure
import org.enso.projectmanager.service.versionmanagement.NoOpInterface
@ -11,7 +14,7 @@ import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
*
* @param distributionConfiguration a distribution configuration
*/
class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel](
class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
distributionConfiguration: DistributionConfiguration
) extends GlobalConfigServiceApi[F] {
@ -20,26 +23,46 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel](
distributionConfiguration.distributionManager
)
/** @inheritdoc */
override def getKey(
key: String
): F[GlobalConfigServiceFailure, Option[String]] =
Sync[F].blockingIO {
Sync[F].blockingOp {
val valueOption = configurationManager.getConfig.original.apply(key)
valueOption.map(json => json.asString.getOrElse(json.toString()))
}.recoverAccessErrors
/** @inheritdoc */
override def setKey(
key: String,
value: String
): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingIO {
): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingOp {
configurationManager.updateConfigRaw(key, Json.fromString(value))
}.recoverAccessErrors
/** @inheritdoc */
override def deleteKey(key: String): F[GlobalConfigServiceFailure, Unit] =
Sync[F].blockingIO {
Sync[F].blockingOp {
configurationManager.removeFromConfig(key)
}.recoverAccessErrors
/** @inheritdoc */
override def getDefaultEnsoVersion: F[GlobalConfigServiceFailure, SemVer] =
Sync[F].blockingOp {
configurationManager.defaultVersion
}.recoverAccessErrors
/** @inheritdoc */
override def resolveEnsoVersion(
ensoVersion: EnsoVersion
): F[GlobalConfigServiceFailure, SemVer] = ensoVersion match {
case DefaultEnsoVersion => getDefaultEnsoVersion
case SemVerEnsoVersion(version) => CovariantFlatMap[F].pure(version)
}
/** Syntax for recovering arbitrary errors into errors describing
* configuration access failure.
*/
implicit class AccessErrorRecovery[A](fa: F[Throwable, A]) {
def recoverAccessErrors: F[GlobalConfigServiceFailure, A] = {
ErrorChannel[F].mapError(fa) { throwable =>
@ -47,4 +70,5 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel](
}
}
}
}

View File

@ -1,5 +1,8 @@
package org.enso.projectmanager.service.config
import nl.gn0s1s.bump.SemVer
import org.enso.pkg.EnsoVersion
/** A contract for the Global Config Service.
*
* @tparam F a monadic context
@ -23,4 +26,19 @@ trait GlobalConfigServiceApi[F[+_, +_]] {
* If the value was not present already, nothing happens.
*/
def deleteKey(key: String): F[GlobalConfigServiceFailure, Unit]
/** Returns the default engine version.
*
* It reads the setting from the config, or if no version is set, falls back
* to the latest installed (or latest available if none are installed)
* version.
*/
def getDefaultEnsoVersion: F[GlobalConfigServiceFailure, SemVer]
/** Resolves an [[EnsoVersion]] which can indicate to use a 'default' version
* to a concrete version.
*/
def resolveEnsoVersion(
ensoVersion: EnsoVersion
): F[GlobalConfigServiceFailure, SemVer]
}

View File

@ -10,6 +10,7 @@ import org.enso.projectmanager.service.ProjectServiceFailure.{
ComponentRepositoryAccessFailure,
ComponentUninstallationFailure
}
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.components.ComponentMissingError
@ -19,9 +20,10 @@ import org.enso.runtimeversionmanager.components.ComponentMissingError
* @param distributionConfiguration a distribution configuration
*/
class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
override val distributionConfiguration: DistributionConfiguration
) extends RuntimeVersionManagementServiceApi[F]
with RuntimeVersionManagerMixin {
distributionConfiguration: DistributionConfiguration
) extends RuntimeVersionManagementServiceApi[F] {
val factory = RuntimeVersionManagerFactory(distributionConfiguration)
/** @inheritdoc */
override def installEngine(
@ -31,11 +33,13 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
): F[ProjectServiceFailure, Unit] = {
Sync[F]
.blockingOp {
makeRuntimeVersionManager(
progressTracker,
allowMissingComponents = true,
allowBrokenComponents = forceInstallBroken
).findOrInstallEngine(version)
factory
.makeRuntimeVersionManager(
progressTracker,
allowMissingComponents = true,
allowBrokenComponents = forceInstallBroken
)
.findOrInstallEngine(version)
()
}
.mapRuntimeManagerErrors(throwable =>
@ -50,11 +54,13 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
): F[ProjectServiceFailure, Unit] = Sync[F]
.blockingOp {
try {
makeRuntimeVersionManager(
progressTracker,
allowMissingComponents = false,
allowBrokenComponents = false
).uninstallEngine(version)
factory
.makeRuntimeVersionManager(
progressTracker,
allowMissingComponents = false,
allowBrokenComponents = false
)
.uninstallEngine(version)
} catch {
case _: ComponentMissingError =>
}
@ -67,7 +73,7 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
override def listInstalledEngines()
: F[ProjectServiceFailure, Seq[EngineVersion]] = Sync[F]
.blockingOp {
makeReadOnlyVersionManager().listInstalledEngines().map {
factory.makeReadOnlyVersionManager().listInstalledEngines().map {
installedEngine =>
EngineVersion(installedEngine.version, installedEngine.isMarkedBroken)
}

View File

@ -0,0 +1,51 @@
package org.enso.projectmanager.service.versionmanagement
import org.enso.projectmanager.control.effect.ErrorChannel
import org.enso.projectmanager.service.ProjectServiceFailure
import org.enso.projectmanager.service.ProjectServiceFailure.{
BrokenComponentFailure,
ComponentInstallationFailure,
MissingComponentFailure,
ProjectManagerUpgradeRequiredFailure
}
import org.enso.runtimeversionmanager.components.{
BrokenComponentError,
ComponentMissingError,
ComponentsException,
InstallationError,
UpgradeRequiredError
}
object RuntimeVersionManagerErrorRecoverySyntax {
implicit class ErrorRecovery[F[+_, +_]: ErrorChannel, A](
fa: F[Throwable, A]
) {
/** Converts relevant [[ComponentsException]] errors into their counterparts
* in the protocol.
*
* @param mapDefault a mapping that should be used for other errors that do
* not have a direct counterpart
*/
def mapRuntimeManagerErrors(
mapDefault: Throwable => ProjectServiceFailure
): F[ProjectServiceFailure, A] = ErrorChannel[F].mapError(fa) {
case componentsException: ComponentsException =>
componentsException match {
case InstallationError(message, _) =>
ComponentInstallationFailure(message)
case BrokenComponentError(message, _) =>
BrokenComponentFailure(message)
case ComponentMissingError(message, _) =>
MissingComponentFailure(message)
case upgradeRequired: UpgradeRequiredError =>
ProjectManagerUpgradeRequiredFailure(
upgradeRequired.expectedVersion
)
case _ => mapDefault(componentsException)
}
case other: Throwable =>
mapDefault(other)
}
}
}

View File

@ -0,0 +1,73 @@
package org.enso.projectmanager.service.versionmanagement
import akka.actor.ActorRef
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.components._
/** A helper class that defines methods for creating the
* [[RuntimeVersionManager]] based on a
* [[DistributionConfiguration]].
*/
case class RuntimeVersionManagerFactory(
distributionConfiguration: DistributionConfiguration
) {
/** Creates a [[RuntimeVersionManager]] that will send
* [[ProgressNotification]] to the specified [[ActorRef]] and with the
* specified settings for handling missing and broken components.
*
* @param progressTracker the actor that tracks installation progress/lock
* notifications
* @param allowMissingComponents if set to true, missing components will be
* installed
* @param allowBrokenComponents if allowMissingComponents and this flag are
* set to true, missing components will be
* installed even if they are marked as broken
*/
def makeRuntimeVersionManager(
progressTracker: ActorRef,
allowMissingComponents: Boolean = false,
allowBrokenComponents: Boolean = false
): RuntimeVersionManager =
distributionConfiguration.makeRuntimeVersionManager(
new ControllerInterface(
progressTracker = progressTracker,
allowMissingComponents = allowMissingComponents,
allowBrokenComponents = allowBrokenComponents
)
)
/** Creates a [[RuntimeVersionManager]] that will send
* [[ProgressNotification]] to the specified [[ActorRef]] and with the
* specified settings for handling missing and broken components.
*
* @param progressTracker the actor that tracks installation progress/lock
* notifications
* @param missingComponentAction specifies how to handle missing components
*/
def makeRuntimeVersionManager(
progressTracker: ActorRef,
missingComponentAction: MissingComponentAction
): RuntimeVersionManager = {
val (missing, broken) = missingComponentAction match {
case MissingComponentAction.Fail => (false, false)
case MissingComponentAction.Install => (true, false)
case MissingComponentAction.ForceInstallBroken => (true, true)
}
makeRuntimeVersionManager(
progressTracker,
allowMissingComponents = missing,
allowBrokenComponents = broken
)
}
/** Creates a simple [[RuntimeVersionManager]] that ignores progress (it can
* be used when we know that no relevant progress will be reported) and not
* allowing to install any components.
*
* It is useful for simple queries, like listing installed versions.
*/
def makeReadOnlyVersionManager(): RuntimeVersionManager =
distributionConfiguration.makeRuntimeVersionManager(new NoOpInterface)
}

View File

@ -1,78 +0,0 @@
package org.enso.projectmanager.service.versionmanagement
import akka.actor.ActorRef
import org.enso.projectmanager.control.effect.ErrorChannel
import org.enso.projectmanager.service.ProjectServiceFailure
import org.enso.projectmanager.service.ProjectServiceFailure.{
BrokenComponentFailure,
ComponentInstallationFailure,
MissingComponentFailure,
ProjectManagerUpgradeRequiredFailure
}
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.components._
/** A helper class that defines methods for creating the
* [[RuntimeVersionManager]] based on a
* [[DistributionConfiguration]].
*/
trait RuntimeVersionManagerMixin {
/** The distribution configuration to use. */
def distributionConfiguration: DistributionConfiguration
/** Creates a [[RuntimeVersionManager]] that will send
* [[ProgressNotification]] to the specified [[ActorRef]] and with the
* specified settings for handling missing and broken components.
*/
def makeRuntimeVersionManager(
progressTracker: ActorRef,
allowMissingComponents: Boolean,
allowBrokenComponents: Boolean
): RuntimeVersionManager =
distributionConfiguration.makeRuntimeVersionManager(
new ControllerInterface(
progressTracker = progressTracker,
allowMissingComponents = allowMissingComponents,
allowBrokenComponents = allowBrokenComponents
)
)
/** Creates a simple [[RuntimeVersionManager]] that ignores progress (it can
* be used when we know that no relevant progress will be reported) and not
* allowing to install any components.
*
* It is useful for simple queries, like listing installed versions.
*/
def makeReadOnlyVersionManager(): RuntimeVersionManager =
distributionConfiguration.makeRuntimeVersionManager(new NoOpInterface)
implicit class ErrorRecovery[F[+_, +_]: ErrorChannel, A](
fa: F[Throwable, A]
) {
/** Converts relevant [[ComponentsException]] errors into their counterparts
* in the protocol.
*/
def mapRuntimeManagerErrors(
wrapDefault: Throwable => ProjectServiceFailure
): F[ProjectServiceFailure, A] = ErrorChannel[F].mapError(fa) {
case componentsException: ComponentsException =>
componentsException match {
case InstallationError(message, _) =>
ComponentInstallationFailure(message)
case BrokenComponentError(message, _) =>
BrokenComponentFailure(message)
case ComponentMissingError(message, _) =>
MissingComponentFailure(message)
case upgradeRequired: UpgradeRequiredError =>
ProjectManagerUpgradeRequiredFailure(
upgradeRequired.expectedVersion
)
case _ => wrapDefault(componentsException)
}
case other: Throwable =>
wrapDefault(other)
}
}
}

View File

@ -16,6 +16,7 @@ import org.enso.runtimeversionmanager.releases.engine.{
EngineRepository
}
import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider
import org.enso.runtimeversionmanager.runner.JVMSettings
/** Default distribution configuration to use for the Project Manager in
* production.
@ -27,13 +28,13 @@ import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider
object DefaultDistributionConfiguration extends DistributionConfiguration {
/** The default [[Environment]] implementation, with no overrides. */
object DefaultEnvironment extends Environment
val environment: Environment = new Environment {}
// TODO [RW, AO] should the PM support portable distributions?
// If so, where will be the project-manager binary located with respect to
// the distribution root?
/** @inheritdoc */
lazy val distributionManager = new DistributionManager(DefaultEnvironment)
lazy val distributionManager = new DistributionManager(environment)
/** @inheritdoc */
lazy val lockManager = new FileLockManager(distributionManager.paths.locks)
@ -63,4 +64,10 @@ object DefaultDistributionConfiguration extends DistributionConfiguration {
engineReleaseProvider = engineReleaseProvider,
runtimeReleaseProvider = runtimeReleaseProvider
)
/** @inheritdoc */
override def defaultJVMSettings: JVMSettings = JVMSettings.default
/** @inheritdoc */
override def shouldDiscardChildOutput: Boolean = false
}

View File

@ -1,5 +1,6 @@
package org.enso.projectmanager.versionmanagement
import org.enso.runtimeversionmanager.Environment
import org.enso.runtimeversionmanager.components.{
RuntimeVersionManagementUserInterface,
RuntimeVersionManager
@ -11,6 +12,7 @@ import org.enso.runtimeversionmanager.distribution.{
import org.enso.runtimeversionmanager.locking.ResourceManager
import org.enso.runtimeversionmanager.releases.ReleaseProvider
import org.enso.runtimeversionmanager.releases.engine.EngineRelease
import org.enso.runtimeversionmanager.runner.JVMSettings
/** Specifies the configuration of project manager's distribution.
*
@ -20,6 +22,9 @@ import org.enso.runtimeversionmanager.releases.engine.EngineRelease
*/
trait DistributionConfiguration {
/** An [[Environment]] instance. */
def environment: Environment
/** A [[DistributionManager]] instance. */
def distributionManager: DistributionManager
@ -39,4 +44,19 @@ trait DistributionConfiguration {
def makeRuntimeVersionManager(
userInterface: RuntimeVersionManagementUserInterface
): RuntimeVersionManager
/** Default set of JVM settings to use when launching the runner.
*
* This is exposed mostly for ease of overriding the settings in tests.
*/
def defaultJVMSettings: JVMSettings
/** Specifies if output of the child Language Server process should be ignored
* or piped to parent's streams.
*
* This option is used to easily turn off logging in tests.
*
* TODO [RW] It will likely become obsolete once #1151 (or #1144) is done.
*/
def shouldDiscardChildOutput: Boolean
}

View File

@ -43,7 +43,7 @@ project-manager {
io-timeout = 5 seconds
request-timeout = 10 seconds
boot-timeout = 30 seconds
shutdown-timeout = 10 seconds
shutdown-timeout = 20 seconds
socket-close-timeout = 2 seconds
}

View File

@ -1,18 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-15thread] %-5level %logger{36} %msg%n</pattern>
</encoder>
</appender>
<logger name="com.zaxxer.hikari" level="ERROR"/>
<logger name="slick" level="INFO"/>
<logger name="slick.compiler" level="INFO"/>
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -1,19 +1,26 @@
package org.enso.projectmanager
import java.io.File
import java.nio.file.Files
import java.nio.file.{Files, Path}
import java.time.{OffsetDateTime, ZoneOffset}
import java.util.UUID
import akka.testkit.TestActors.blackholeProps
import akka.testkit._
import io.circe.Json
import io.circe.parser.parse
import nl.gn0s1s.bump.SemVer
import org.apache.commons.io.FileUtils
import org.enso.jsonrpc.test.JsonRpcServerTestKit
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
import org.enso.loggingservice.printers.StderrPrinterWithColors
import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager}
import org.enso.projectmanager.boot.Globals.{ConfigFilename, ConfigNamespace}
import org.enso.projectmanager.boot.configuration._
import org.enso.projectmanager.control.effect.ZioEnvExec
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
import org.enso.projectmanager.infrastructure.languageserver.{
ExecutorWithUnlimitedPool,
LanguageServerGatewayImpl,
LanguageServerRegistry,
ShutdownHookActivator
@ -26,18 +33,27 @@ import org.enso.projectmanager.protocol.{
}
import org.enso.projectmanager.service.config.GlobalConfigService
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService
import org.enso.projectmanager.service.{MonadicProjectValidator, ProjectService}
import org.enso.projectmanager.service.{
MonadicProjectValidator,
ProjectCreationService,
ProjectService
}
import org.enso.projectmanager.test.{ObservableGenerator, ProgrammableClock}
import org.enso.runtimeversionmanager.OS
import org.enso.runtimeversionmanager.test.{DropLogs, FakeReleases}
import org.scalatest.BeforeAndAfterAll
import pureconfig.ConfigSource
import pureconfig.generic.auto._
import zio.interop.catz.core._
import zio.{Runtime, Semaphore, ZEnv, ZIO}
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
class BaseServerSpec
extends JsonRpcServerTestKit
with DropLogs
with BeforeAndAfterAll {
override def protocol: Protocol = JsonRpc.protocol
@ -97,10 +113,25 @@ class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
lazy val projectValidator = new MonadicProjectValidator[ZIO[ZEnv, *, *]]()
lazy val distributionConfiguration =
TestDistributionConfiguration(
distributionRoot = testDistributionRoot.toPath,
engineReleaseProvider = FakeReleases.engineReleaseProvider,
runtimeReleaseProvider = FakeReleases.runtimeReleaseProvider,
discardChildOutput = !debugChildLogs
)
lazy val languageServerRegistry =
system.actorOf(
LanguageServerRegistry
.props(netConfig, bootloaderConfig, supervisionConfig, timeoutConfig)
.props(
netConfig,
bootloaderConfig,
supervisionConfig,
timeoutConfig,
distributionConfiguration,
ExecutorWithUnlimitedPool
)
)
lazy val shutdownHookActivator =
@ -114,27 +145,26 @@ class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
timeoutConfig
)
lazy val projectService =
new ProjectService[ZIO[ZEnv, +*, +*]](
projectValidator,
projectRepository,
new Slf4jLogging[ZIO[ZEnv, +*, +*]],
testClock,
gen,
languageServerGateway
)
lazy val distributionConfiguration =
TestDistributionConfiguration(
distributionRoot = testDistributionRoot.toPath,
engineReleaseProvider = FakeReleases.engineReleaseProvider,
runtimeReleaseProvider = FakeReleases.runtimeReleaseProvider
)
lazy val projectCreationService =
new ProjectCreationService[ZIO[ZEnv, +*, +*]](distributionConfiguration)
lazy val globalConfigService = new GlobalConfigService[ZIO[ZEnv, +*, +*]](
distributionConfiguration
)
lazy val projectService =
new ProjectService[ZIO[ZEnv, +*, +*]](
projectValidator,
projectRepository,
projectCreationService,
globalConfigService,
new Slf4jLogging[ZIO[ZEnv, +*, +*]],
testClock,
gen,
languageServerGateway,
distributionConfiguration
)
lazy val runtimeVersionManagementService =
new RuntimeVersionManagementService[ZIO[ZEnv, +*, +*]](
distributionConfiguration
@ -150,9 +180,141 @@ class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
)
}
/** Can be used to avoid deleting the project's root. */
val deleteProjectsRootAfterEachTest = true
override def afterEach(): Unit = {
super.afterEach()
if (deleteProjectsRootAfterEachTest)
FileUtils.deleteQuietly(testProjectsRoot)
}
override def afterAll(): Unit = {
super.afterAll()
FileUtils.deleteQuietly(testProjectsRoot)
}
/** Tests can override this value to request a specific engine version to be
* preinstalled when running the suite.
*/
val engineToInstall: Option[SemVer] = None
/** Tests can override this to set up a logging service that will print debug
* logs.
*/
val debugLogs: Boolean = false
/** Tests can override this to allow child process output to be displayed. */
val debugChildLogs: Boolean = false
override def beforeAll(): Unit = {
super.beforeAll()
if (debugLogs) {
LoggingServiceManager.setup(
LoggerMode.Local(
Seq(StderrPrinterWithColors.colorPrinterIfAvailable(true))
),
LogLevel.Trace
)
}
engineToInstall.foreach(preInstallEngine)
}
/** This is a temporary solution to ensure that a valid engine distribution is
* preinstalled.
*
* In the future the fake release mechanism can be properly updated to allow
* for this kind of configuration without special logic.
*/
def preInstallEngine(version: SemVer): Unit = {
val os = OS.operatingSystem.configName
val ext = if (OS.isWindows) "zip" else "tar.gz"
val arch = OS.architecture
val path = FakeReleases.releaseRoot
.resolve("enso")
.resolve(s"enso-$version")
.resolve(s"enso-engine-$version-$os-$arch.$ext")
.resolve(s"enso-$version")
.resolve("component")
val root = Path.of("../../../").toAbsolutePath.normalize
FileUtils.copyFile(
root.resolve("runner.jar").toFile,
path.resolve("runner.jar").toFile
)
FileUtils.copyFile(
root.resolve("runtime.jar").toFile,
path.resolve("runtime.jar").toFile
)
val blackhole = system.actorOf(blackholeProps)
val installAction = runtimeVersionManagementService.installEngine(
blackhole,
version,
forceInstallBroken = false
)
Runtime.default.unsafeRun(installAction)
}
def uninstallEngine(version: SemVer): Unit = {
val blackhole = system.actorOf(blackholeProps)
val action = runtimeVersionManagementService.uninstallEngine(
blackhole,
version
)
Runtime.default.unsafeRun(action)
}
implicit class ClientSyntax(client: WsTestClient) {
def expectTaskStarted(
timeout: FiniteDuration = 20.seconds.dilated
): Unit = {
inside(parse(client.expectMessage(timeout))) { case Right(json) =>
getMethod(json) shouldEqual Some("task/started")
}
}
private def getMethod(json: Json): Option[String] = for {
obj <- json.asObject
method <- obj("method").flatMap(_.asString)
} yield method
def expectJsonIgnoring(
shouldIgnore: Json => Boolean,
timeout: FiniteDuration = 20.seconds.dilated
): Json = {
inside(parse(client.expectMessage(timeout))) { case Right(json) =>
if (shouldIgnore(json)) expectJsonIgnoring(shouldIgnore, timeout)
else json
}
}
def expectError(
expectedCode: Int,
timeout: FiniteDuration = 10.seconds.dilated
): Unit = {
withClue("Response should be an error: ") {
inside(parse(client.expectMessage(timeout))) { case Right(json) =>
val code = for {
obj <- json.asObject
error <- obj("error").flatMap(_.asObject)
code <- error("code").flatMap(_.asNumber).flatMap(_.toInt)
} yield code
code shouldEqual Some(expectedCode)
}
}
}
def expectJsonAfterSomeProgress(
json: Json,
timeout: FiniteDuration = 10.seconds.dilated
): Unit =
expectJsonIgnoring(
json => getMethod(json).exists(_.startsWith("task/")),
timeout
) shouldEqual json
}
}

View File

@ -2,6 +2,7 @@ package org.enso.projectmanager
import java.util.UUID
import akka.testkit.TestDuration
import io.circe.Json
import io.circe.syntax._
import io.circe.literal._
@ -58,7 +59,7 @@ trait ProjectManagementOps { this: BaseServerSpec =>
}
}
""")
val Right(openReply) = parse(client.expectMessage(10.seconds))
val Right(openReply) = parse(client.expectMessage(10.seconds.dilated))
val socket = for {
result <- openReply.hcursor.downExpectedField("result")
addr <- result.downExpectedField("languageServerJsonAddress")
@ -81,13 +82,16 @@ trait ProjectManagementOps { this: BaseServerSpec =>
}
}
""")
client.expectJson(json"""
client.expectJson(
json"""
{
"jsonrpc":"2.0",
"id":0,
"result": null
}
""")
""",
10.seconds.dilated
)
}
def deleteProject(

View File

@ -25,12 +25,14 @@ import org.enso.runtimeversionmanager.releases.{
ReleaseProvider,
SimpleReleaseProvider
}
import org.enso.runtimeversionmanager.runner.{JVMSettings, JavaCommand}
import org.enso.runtimeversionmanager.test.{
FakeEnvironment,
HasTestDirectory,
TestLocalLockManager
}
import scala.jdk.OptionConverters.RichOptional
import scala.util.{Failure, Success, Try}
/** A distribution configuration for use in tests.
@ -39,20 +41,23 @@ import scala.util.{Failure, Success, Try}
* within some temporary directory
* @param engineReleaseProvider provider of (fake) engine releases
* @param runtimeReleaseProvider provider of (fake) Graal releases
* @param discardChildOutput specifies if input of launched runner processes
* should be ignored
*/
class TestDistributionConfiguration(
distributionRoot: Path,
override val engineReleaseProvider: ReleaseProvider[EngineRelease],
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider,
discardChildOutput: Boolean
) extends DistributionConfiguration
with FakeEnvironment
with HasTestDirectory {
def getTestDirectory: Path = distributionRoot
lazy val distributionManager = new DistributionManager(
fakeInstalledEnvironment()
)
lazy val environment = fakeInstalledEnvironment()
lazy val distributionManager = new DistributionManager(environment)
lazy val lockManager = new TestLocalLockManager
@ -71,36 +76,66 @@ class TestDistributionConfiguration(
engineReleaseProvider = engineReleaseProvider,
runtimeReleaseProvider = runtimeReleaseProvider
)
}
object TestDistributionConfiguration {
def withoutReleases(distributionRoot: Path): TestDistributionConfiguration = {
val noReleaseProvider = new SimpleReleaseProvider {
override def releaseForTag(tag: String): Try[Release] = Failure(
new IllegalStateException(
"This provider does not support fetching releases."
)
)
override def listReleases(): Try[Seq[Release]] = Success(Seq())
}
new TestDistributionConfiguration(
distributionRoot = distributionRoot,
engineReleaseProvider = new EngineReleaseProvider(noReleaseProvider),
runtimeReleaseProvider = new GraalCEReleaseProvider(noReleaseProvider)
/** JVM settings that will force to use the same JVM that we are running.
*
* This is done to avoiding downloading GraalVM in tests (that would be far
* too slow) and to ensure that a GraalVM instance is selected, regardless of
* the default JVM set in the current environment.
*/
override def defaultJVMSettings: JVMSettings = {
val currentProcess =
ProcessHandle.current().info().command().toScala.getOrElse("java")
val javaCommand = JavaCommand(currentProcess, None)
new JVMSettings(
javaCommandOverride = Some(javaCommand),
jvmOptions = Seq()
)
}
override def shouldDiscardChildOutput: Boolean = discardChildOutput
}
object TestDistributionConfiguration {
/** Creates a [[TestDistributionConfiguration]] with repositories that do not
* have any available releases.
*/
def withoutReleases(
distributionRoot: Path,
discardChildOutput: Boolean
): TestDistributionConfiguration = {
val noReleaseProvider = new NoReleaseProvider
new TestDistributionConfiguration(
distributionRoot = distributionRoot,
engineReleaseProvider = new EngineReleaseProvider(noReleaseProvider),
runtimeReleaseProvider = new GraalCEReleaseProvider(noReleaseProvider),
discardChildOutput
)
}
/** Creates a [[TestDistributionConfiguration]] instance. */
def apply(
distributionRoot: Path,
engineReleaseProvider: ReleaseProvider[EngineRelease],
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider,
discardChildOutput: Boolean
): TestDistributionConfiguration =
new TestDistributionConfiguration(
distributionRoot,
engineReleaseProvider,
runtimeReleaseProvider
runtimeReleaseProvider,
discardChildOutput
)
/** A [[SimpleReleaseProvider]] that has no releases. */
private class NoReleaseProvider extends SimpleReleaseProvider {
override def releaseForTag(tag: String): Try[Release] = Failure(
new IllegalStateException(
"This provider does not support fetching releases."
)
)
override def listReleases(): Try[Seq[Release]] = Success(Seq())
}
}

View File

@ -1,8 +1,10 @@
package org.enso.projectmanager.infrastructure.languageserver
import akka.testkit.TestDuration
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.test.Net._
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.testkit.FlakySpec
import org.enso.testkit.{FlakySpec, RetrySpec}
import scala.concurrent.Await
import scala.concurrent.duration._
@ -10,11 +12,14 @@ import scala.concurrent.duration._
class LanguageServerGatewaySpec
extends BaseServerSpec
with FlakySpec
with ProjectManagementOps {
with ProjectManagementOps
with RetrySpec {
override val engineToInstall = Some(SemVer(0, 0, 1))
"A language server service" must {
"kill all running language servers" ignore {
"kill all running language servers" taggedAs Retry ignore {
implicit val client = new WsTestClient(address)
val fooId = createProject("foo")
val barId = createProject("bar")
@ -27,7 +32,7 @@ class LanguageServerGatewaySpec
tryConnect(bazSocket).isRight shouldBe true
//when
val future = exec.exec(languageServerGateway.killAllServers())
Await.result(future, 20.seconds)
Await.result(future, 30.seconds.dilated)
//then
tryConnect(fooSocket).isLeft shouldBe true
tryConnect(barSocket).isLeft shouldBe true

View File

@ -1,15 +1,11 @@
package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID
import akka.actor.{ActorSystem, Props}
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.testkit.{ImplicitSender, TestActor, TestKit, TestProbe}
import com.miguno.akka.testing.VirtualTime
import org.enso.languageserver.boot.LifecycleComponent.ComponentRestarted
import org.enso.languageserver.boot.{LanguageServerConfig, LifecycleComponent}
import org.enso.projectmanager.boot.configuration.SupervisionConfig
import org.enso.projectmanager.infrastructure.http.AkkaBasedWebSocketConnectionFactory
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.ServerDied
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.ServerBooted
import org.enso.projectmanager.infrastructure.languageserver.ProgrammableWebSocketServer.{
Reject,
ReplyWith
@ -17,14 +13,11 @@ import org.enso.projectmanager.infrastructure.languageserver.ProgrammableWebSock
import org.enso.projectmanager.infrastructure.languageserver.StepParent.ChildTerminated
import org.enso.projectmanager.infrastructure.net.Tcp
import org.enso.testkit.FlakySpec
import org.mockito.BDDMockito._
import org.mockito.Mockito._
import org.mockito.MockitoSugar
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AnyFlatSpecLike
import org.scalatest.matchers.must.Matchers
import scala.concurrent.Future
import scala.concurrent.duration._
class LanguageServerSupervisorSpec
@ -55,7 +48,7 @@ class LanguageServerSupervisorSpec
virtualTimeAdvances(testHeartbeatInterval / 2)
}
//then
`then`(serverComponent.restart()).shouldHaveNoInteractions()
processManagerProbe.expectNoMessage()
//teardown
parent ! GracefulStop
parentProbe.expectMsg(ChildTerminated)
@ -65,8 +58,6 @@ class LanguageServerSupervisorSpec
it should "restart server when pong message doesn't arrive on time" taggedAs Flaky in new TestCtx {
//given
when(serverComponent.restart())
.thenReturn(Future.successful(ComponentRestarted))
val probe = TestProbe()
@volatile var pingCount = 0
fakeServer.withBehaviour { case ping @ PingMatcher(requestId) =>
@ -84,7 +75,7 @@ class LanguageServerSupervisorSpec
//when
virtualTimeAdvances(testInitialDelay)
(1 to 2).foreach { _ =>
verifyNoInteractions(serverComponent)
processManagerProbe.expectNoMessage()
probe.expectMsgPF() { case PingMatcher(_) => () }
virtualTimeAdvances(testHeartbeatInterval / 2)
probe.expectNoMessage()
@ -92,10 +83,11 @@ class LanguageServerSupervisorSpec
}
probe.expectMsgPF() { case PingMatcher(_) => () }
virtualTimeAdvances(testHeartbeatTimeout)
verify(serverComponent, timeout(VerificationTimeout).times(1)).restart()
processManagerProbe.expectMsg(Restart)
restartRequests mustEqual 1
virtualTimeAdvances(testInitialDelay)
(1 to 2).foreach { _ =>
verifyNoMoreInteractions(serverComponent)
processManagerProbe.expectNoMessage()
probe.expectMsgPF() { case PingMatcher(_) => () }
virtualTimeAdvances(testHeartbeatInterval / 2)
probe.expectNoMessage()
@ -108,35 +100,6 @@ class LanguageServerSupervisorSpec
fakeServer.stop()
}
it should "restart server limited number of times" in new TestCtx {
//given
when(serverComponent.restart()).thenReturn(Future.failed(new Exception))
val probe = TestProbe()
fakeServer.withBehaviour { case ping @ PingMatcher(_) =>
probe.ref ! ping
Reject
}
probe.expectNoMessage()
//when
virtualTimeAdvances(testInitialDelay)
probe.expectMsgPF(5.seconds) { case PingMatcher(_) => () }
verifyNoInteractions(serverComponent)
virtualTimeAdvances(testHeartbeatTimeout)
(1 to testRestartLimit).foreach { i =>
verify(serverComponent, timeout(VerificationTimeout).times(i)).restart()
virtualTimeAdvances(testRestartDelay)
}
virtualTimeAdvances(testHeartbeatInterval)
probe.expectNoMessage()
verifyNoMoreInteractions(serverComponent)
//then
parentProbe.expectMsg(ServerDied)
parentProbe.expectMsg(ChildTerminated)
//teardown
system.stop(parent)
fakeServer.stop()
}
override def afterAll(): Unit = {
TestKit.shutdownActorSystem(system)
}
@ -147,8 +110,6 @@ class LanguageServerSupervisorSpec
val virtualTime = new VirtualTime
val serverComponent = mock[LifecycleComponent]
val testHost = "127.0.0.1"
val testRpcPort = Tcp.findAvailablePort(testHost, 49152, 55535)
@ -168,13 +129,11 @@ class LanguageServerSupervisorSpec
val fakeServer = new ProgrammableWebSocketServer(testHost, testRpcPort)
fakeServer.start()
val serverConfig =
LanguageServerConfig(
val connectionInfo =
LanguageServerConnectionInfo(
testHost,
testRpcPort,
testDataPort,
UUID.randomUUID(),
"/tmp"
testDataPort
)
val supervisionConfig =
@ -188,15 +147,28 @@ class LanguageServerSupervisorSpec
val parentProbe = TestProbe()
val processManagerProbe = TestProbe()
var restartRequests = 0
processManagerProbe
.setAutoPilot((sender: ActorRef, msg: Any) =>
msg match {
case Restart =>
restartRequests += 1
sender ! ServerBooted(connectionInfo, processManagerProbe.ref)
TestActor.KeepRunning
case _ => TestActor.KeepRunning
}
)
val parent = system.actorOf(
Props(
new StepParent(
LanguageServerSupervisor.props(
serverConfig,
serverComponent,
supervisionConfig,
new AkkaBasedWebSocketConnectionFactory(),
virtualTime.scheduler
connectionInfo = connectionInfo,
serverProcessManager = processManagerProbe.ref,
supervisionConfig = supervisionConfig,
connectionFactory = new AkkaBasedWebSocketConnectionFactory(),
scheduler = virtualTime.scheduler
),
parentProbe.ref
)

View File

@ -64,7 +64,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
}
}
""")
client.expectJson(
client.expectTaskStarted()
client.expectJsonAfterSomeProgress(
json"""
{
"jsonrpc":"2.0",
@ -84,7 +85,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
}
}
""")
client.expectJson(
client.expectTaskStarted()
client.expectJsonAfterSomeProgress(
json"""
{
"jsonrpc":"2.0",
@ -164,11 +166,15 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
}
}
""")
val message =
"Installation has been cancelled by the user because the requested " +
"engine release is marked as broken."
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":0,
"error": { "code": 4021, "message": "Installation has been cancelled by the user because the requested engine release is marked as broken." }
"error": { "code": 4021, "message": $message }
}
""")
@ -182,7 +188,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
}
}
""")
client.expectJson(
client.expectTaskStarted()
client.expectJsonAfterSomeProgress(
json"""
{
"jsonrpc":"2.0",

View File

@ -0,0 +1,58 @@
package org.enso.projectmanager.protocol
import io.circe.Json
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.BaseServerSpec
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.testkit.RetrySpec
import org.scalatest.wordspec.AnyWordSpecLike
trait MissingComponentBehavior {
this: BaseServerSpec with AnyWordSpecLike with RetrySpec =>
def buildRequest(
version: SemVer,
missingComponentAction: MissingComponentAction
): Json
def isSuccess(json: Json): Boolean
private val defaultVersion = SemVer(0, 0, 1)
private val brokenVersion = SemVer(0, 999, 0, Some("broken"))
def correctlyHandleMissingComponents(): Unit = {
"fail if a missing version is requested with Fail" in {
val client = new WsTestClient(address)
client.send(buildRequest(defaultVersion, MissingComponentAction.Fail))
client.expectError(4020)
}
"install the missing version and succeed with Install" taggedAs Retry in {
val client = new WsTestClient(address)
client.send(
buildRequest(defaultVersion, MissingComponentAction.Install)
)
/** We do not check for success here as we are concerned onyl that the
* installation is attempted. Installation and creating/opening projects
* are tested elsewhere.
*/
client.expectTaskStarted()
}
"fail if the requested missing version is marked as broken with " +
"Install" in {
val client = new WsTestClient(address)
client.send(buildRequest(brokenVersion, MissingComponentAction.Install))
client.expectError(4021)
}
"succeed even if the requested missing version is marked as broken " +
"with ForceInstallBroken" taggedAs Retry in {
val client = new WsTestClient(address)
client.send(
buildRequest(brokenVersion, MissingComponentAction.ForceInstallBroken)
)
client.expectTaskStarted()
}
}
}

View File

@ -0,0 +1,44 @@
package org.enso.projectmanager.protocol
import io.circe.Json
import io.circe.literal.JsonStringContext
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.testkit.RetrySpec
import org.enso.pkg.SemVerJson._
class ProjectCreateMissingComponentsSpec
extends BaseServerSpec
with RetrySpec
with ProjectManagementOps
with MissingComponentBehavior {
override def buildRequest(
version: SemVer,
missingComponentAction: MissingComponentAction
): Json =
json"""
{ "jsonrpc": "2.0",
"method": "project/create",
"id": 1,
"params": {
"name": "testproj",
"missingComponentAction": $missingComponentAction,
"version": $version
}
}
"""
override def isSuccess(json: Json): Boolean = {
val projectId = for {
obj <- json.asObject
result <- obj("result").flatMap(_.asObject)
id <- result("projectId")
} yield id
projectId.isDefined
}
"project/create" should {
behave like correctlyHandleMissingComponents()
}
}

View File

@ -5,6 +5,7 @@ import java.nio.file.Paths
import java.util.UUID
import io.circe.literal._
import nl.gn0s1s.bump.SemVer
import org.apache.commons.io.FileUtils
import org.enso.projectmanager.test.Net.tryConnect
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
@ -22,6 +23,8 @@ class ProjectManagementApiSpec
gen.reset()
}
override val engineToInstall = Some(SemVer(0, 0, 1))
"project/create" must {
"check if project name is not empty" taggedAs Flaky in {
@ -31,7 +34,8 @@ class ProjectManagementApiSpec
"method": "project/create",
"id": 1,
"params": {
"name": ""
"name": "",
"missingComponentAction": "Install"
}
}
""")
@ -124,26 +128,23 @@ class ProjectManagementApiSpec
}
"create project with specific version" in {
// TODO [RW] this is just a stub test for parsing, it should be replaced
// with actual tests once this functionality is implemented
implicit val client = new WsTestClient(address)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "project/create",
"id": 0,
"id": 1,
"params": {
"name": "foo",
"version": "1.2.3"
"version": "0.0.1"
}
}
""")
client.expectJson(json"""
{
"jsonrpc":"2.0",
"id":0,
"error":{
"code":10,
"message":"The requested method is not implemented"
"jsonrpc" : "2.0",
"id" : 1,
"result" : {
"projectId" : $getGeneratedUUID
}
}
""")

View File

@ -0,0 +1,91 @@
package org.enso.projectmanager.protocol
import java.io.File
import java.util.UUID
import akka.testkit.TestActors.blackholeProps
import io.circe.Json
import io.circe.literal.JsonStringContext
import nl.gn0s1s.bump.SemVer
import org.enso.pkg.SemVerEnsoVersion
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.testkit.RetrySpec
import zio.Runtime
class ProjectOpenMissingComponentsSpec
extends BaseServerSpec
with RetrySpec
with ProjectManagementOps
with MissingComponentBehavior {
val ordinaryVersion = SemVer(0, 0, 1)
override val engineToInstall: Option[SemVer] = Some(ordinaryVersion)
var ordinaryProject: UUID = _
var brokenProject: UUID = _
override val deleteProjectsRootAfterEachTest = false
override def beforeAll(): Unit = {
super.beforeAll()
val blackhole = system.actorOf(blackholeProps)
val ordinaryAction = projectService.createUserProject(
blackhole,
"proj1",
ordinaryVersion,
MissingComponentAction.Fail
)
ordinaryProject = Runtime.default.unsafeRun(ordinaryAction)
val brokenName = "projbroken"
val brokenAction = projectService.createUserProject(
blackhole,
brokenName,
ordinaryVersion,
MissingComponentAction.Fail
)
brokenProject = Runtime.default.unsafeRun(brokenAction)
// TODO [RW] this hack should not be necessary with #1273
val projectDir = new File(userProjectDir, brokenName)
val pkgManager = org.enso.pkg.PackageManager.Default
val pkg = pkgManager.loadPackage(projectDir).get
pkg.updateConfig(config =>
config.copy(ensoVersion =
SemVerEnsoVersion(SemVer(0, 999, 0, Some("broken")))
)
)
uninstallEngine(ordinaryVersion)
}
override def buildRequest(
version: SemVer,
missingComponentAction: MissingComponentAction
): Json = {
val projectId =
if (version.preRelease.contains("broken")) brokenProject
else ordinaryProject
json"""
{ "jsonrpc": "2.0",
"method": "project/open",
"id": 1,
"params": {
"projectId": $projectId,
"missingComponentAction": $missingComponentAction
}
}
"""
}
override def isSuccess(json: Json): Boolean = {
val result = for {
obj <- json.asObject
result <- obj("result").flatMap(_.asObject)
} yield result
result.isDefined
}
"project/open" should {
behave like correctlyHandleMissingComponents()
}
}

View File

@ -1,3 +1,12 @@
minimum-launcher-version: 0.0.1
graal-vm-version: 2.0.0
graal-java-version: 11
jvm-options:
- value: "-Dpolyglot.engine.IterativePartialEscape=true"
- value: "-Dtruffle.class.path.append=$enginePackagePath\\component\\runtime.jar"
os: "windows"
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
os: "linux"
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
os: "macos"
- value: "-Denso.version.override=0.0.1"

View File

@ -4,7 +4,7 @@ import buildinfo.Info
import com.typesafe.scalalogging.Logger
import nl.gn0s1s.bump.SemVer
/** Helper object that allows to get the current launcher version.
/** Helper object that allows to get the current application version.
*
* In development-mode it allows to override the returned version for testing
* purposes.

View File

@ -4,8 +4,13 @@ import java.nio.file.{Files, Path, StandardOpenOption}
import com.typesafe.scalalogging.Logger
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.FileSystem
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.archive.Archive
import org.enso.runtimeversionmanager.distribution.{
DistributionManager,
TemporaryDirectoryManager
}
import org.enso.runtimeversionmanager.locking.{
LockType,
Resource,
@ -14,11 +19,6 @@ import org.enso.runtimeversionmanager.locking.{
import org.enso.runtimeversionmanager.releases.ReleaseProvider
import org.enso.runtimeversionmanager.releases.engine.EngineRelease
import org.enso.runtimeversionmanager.releases.graalvm.GraalVMRuntimeReleaseProvider
import org.enso.runtimeversionmanager.FileSystem
import org.enso.runtimeversionmanager.distribution.{
DistributionManager,
TemporaryDirectoryManager
}
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try, Using}

View File

@ -20,18 +20,7 @@ class ResourceManager(lockManager: LockManager) {
resource: Resource,
lockType: LockType
)(action: => R): R = {
var waited = false
Using {
lockManager.acquireLockWithWaitingAction(
resource.name,
lockType = lockType,
() => {
waited = true
waitingInterface.startWaitingForResource(resource)
}
)
} { _ =>
if (waited) waitingInterface.finishWaitingForResource(resource)
Using(acquireResource(waitingInterface, resource, lockType)) { _ =>
action
}.get
}
@ -52,6 +41,27 @@ class ResourceManager(lockManager: LockManager) {
action
}
/** Acquires a resource, handling possible waiting, and returns its [[Lock]]
* instance that can be used to unlock it.
*/
private def acquireResource(
waitingInterface: LockUserInterface,
resource: Resource,
lockType: LockType
): Lock = {
var waited = false
val lock = lockManager.acquireLockWithWaitingAction(
resource.name,
lockType = lockType,
() => {
waited = true
waitingInterface.startWaitingForResource(resource)
}
)
if (waited) waitingInterface.finishWaitingForResource(resource)
lock
}
var mainLock: Option[Lock] = None
/** Initializes the [[MainLock]].

View File

@ -21,10 +21,7 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
def run(): Try[Int] =
wrapError {
logger.debug(s"Executing $toString")
val processBuilder = new java.lang.ProcessBuilder(command: _*)
for ((key, value) <- extraEnv) {
processBuilder.environment().put(key, value)
}
val processBuilder = builder()
processBuilder.inheritIO()
val process = processBuilder.start()
process.waitFor()
@ -44,6 +41,21 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
processBuilder.!!
}
/** Returns a [[ProcessBuilder]] that can be used to start the process.
*
* This is an advanced feature and it has to be used very carefully - the
* builder and the constructed process (as long as it is running) must not
* leak outside of the enclosing `withCommand` function to preserve the
* guarantees that the environment the process requires still exists.
*/
def builder(): ProcessBuilder = {
val processBuilder = new java.lang.ProcessBuilder(command: _*)
for ((key, value) <- extraEnv) {
processBuilder.environment().put(key, value)
}
processBuilder
}
/** Runs the provided action and wraps any errors into a [[Failure]]
* containing a [[RunnerError]].
*/

View File

@ -2,8 +2,37 @@ package org.enso.runtimeversionmanager.runner
/** Represents settings that are used to launch the runtime JVM.
*
* @param useSystemJVM if set, the system configured JVM is used instead of
* the one managed by the launcher
* @param javaCommandOverride the command should be used to launch the JVM
* instead of the default JVM provided with the
* release; it can be an absolute path to a java
* executable
* @param jvmOptions options that should be added to the launched JVM
*/
case class JVMSettings(useSystemJVM: Boolean, jvmOptions: Seq[(String, String)])
case class JVMSettings(
javaCommandOverride: Option[JavaCommand],
jvmOptions: Seq[(String, String)]
)
object JVMSettings {
/** Creates settings that are used to launch the runtime JVM.
*
* @param useSystemJVM if set, the system configured JVM is used instead of
* the one managed by the launcher
* @param jvmOptions options that should be added to the launched JVM
*/
def apply(
useSystemJVM: Boolean,
jvmOptions: Seq[(String, String)]
): JVMSettings =
new JVMSettings(
if (useSystemJVM) Some(JavaCommand.systemJavaCommand) else None,
jvmOptions
)
/** Creates a default instance of [[JVMSettings]] that just use the default
* JVM with no options overrides.
*/
def default: JVMSettings =
JVMSettings(useSystemJVM = false, jvmOptions = Seq())
}

View File

@ -0,0 +1,31 @@
package org.enso.runtimeversionmanager.runner
import org.enso.runtimeversionmanager.components.GraalRuntime
/** Represents a way of launching the JVM.
*
* @param executableName name of the `java` executable to run
* @param javaHomeOverride if set, asks to override the JAVA_HOME environment
* variable when launching the JVM
*/
case class JavaCommand(
executableName: String,
javaHomeOverride: Option[String]
)
object JavaCommand {
/** The [[JavaCommand]] representing the system-configured JVM.
*/
def systemJavaCommand: JavaCommand = JavaCommand("java", None)
/** The [[JavaCommand]] representing a managed [[GraalRuntime]].
*/
def forRuntime(runtime: GraalRuntime): JavaCommand =
JavaCommand(
executableName = runtime.javaExecutable.toAbsolutePath.normalize.toString,
javaHomeOverride =
Some(runtime.javaHome.toAbsolutePath.normalize.toString)
)
}

View File

@ -4,13 +4,13 @@ import nl.gn0s1s.bump.SemVer
/** Represents settings that are used to launch the runner JAR.
*
* @param version Enso engine version to use
* @param engineVersion Enso engine version to use
* @param runnerArguments arguments that should be passed to the runner
* @param connectLoggerIfAvailable specifies if the ran component should
* connect to launcher's logging service
*/
case class RunSettings(
version: SemVer,
engineVersion: SemVer,
runnerArguments: Seq[String],
connectLoggerIfAvailable: Boolean
)

Some files were not shown because too many files have changed in this diff Show More