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

View File

@ -609,6 +609,26 @@ lazy val `logging-service` = project
) )
.dependsOn(`akka-native`) .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 lazy val cli = project
.in(file("lib/scala/cli")) .in(file("lib/scala/cli"))
.configs(Test) .configs(Test)
@ -647,15 +667,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
(Compile / run / fork) := true, (Compile / run / fork) := true,
(Test / fork) := true, (Test / fork) := true,
(Compile / run / connectInput) := true, (Compile / run / connectInput) := true,
javaOptions ++= { libraryDependencies ++= akka ++ Seq(akkaTestkit % Test),
// Note [Classpath Separation]
val runtimeClasspath =
(runtime / Compile / fullClasspath).value
.map(_.data)
.mkString(File.pathSeparator)
Seq(s"-Dtruffle.class.path.append=$runtimeClasspath")
},
libraryDependencies ++= akka,
libraryDependencies ++= circe, libraryDependencies ++= circe,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"com.typesafe" % "config" % typesafeConfigVersion, "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, "dev.zio" %% "zio-interop-cats" % zioInteropCatsVersion,
"commons-cli" % "commons-cli" % commonsCliVersion, "commons-cli" % "commons-cli" % commonsCliVersion,
"commons-io" % "commons-io" % commonsIoVersion, "commons-io" % "commons-io" % commonsIoVersion,
"org.apache.commons" % "commons-lang3" % commonsLangVersion,
"com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion, "com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion,
"com.miguno.akka" %% "akka-mock-scheduler" % akkaMockSchedulerVersion % Test, "com.miguno.akka" %% "akka-mock-scheduler" % akkaMockSchedulerVersion % Test,
"org.mockito" %% "mockito-scala" % mockitoScalaVersion % Test "org.mockito" %% "mockito-scala" % mockitoScalaVersion % Test
@ -697,13 +710,12 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
) )
) )
), ),
assembly := assembly assembly := assembly.dependsOn(`engine-runner` / assembly).value,
.dependsOn(runtime / assembly) (Test / test) := (Test / test).dependsOn(`engine-runner` / assembly).value
.value
) )
.dependsOn(`version-output`) .dependsOn(`version-output`)
.dependsOn(pkg) .dependsOn(pkg)
.dependsOn(`language-server`) .dependsOn(`polyglot-api`)
.dependsOn(`runtime-version-manager`) .dependsOn(`runtime-version-manager`)
.dependsOn(`json-rpc-server`) .dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test) .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")) lazy val `language-server` = (project in file("engine/language-server"))
.settings( .settings(
libraryDependencies ++= akka ++ akkaTest ++ circe ++ Seq( libraryDependencies ++= akka ++ circe ++ Seq(
"ch.qos.logback" % "logback-classic" % logbackClassicVersion,
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"io.circe" %% "circe-generic-extras" % circeGenericExtrasVersion, "io.circe" %% "circe-generic-extras" % circeGenericExtrasVersion,
"io.circe" %% "circe-literal" % circeVersion, "io.circe" %% "circe-literal" % circeVersion,
@ -902,6 +913,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
.dependsOn(`text-buffer`) .dependsOn(`text-buffer`)
.dependsOn(`searcher`) .dependsOn(`searcher`)
.dependsOn(testkit % Test) .dependsOn(testkit % Test)
.dependsOn(`logging-service`)
lazy val ast = (project in file("lib/scala/ast")) lazy val ast = (project in file("lib/scala/ast"))
.settings( .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`. 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. '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`. 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`. 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`. 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. '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. 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`. 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 #### 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. 1. Open the review in edit mode using the helper script.
- You can type `enso / openLegalReviewReport` if you have `npm` in your PATH - You can type `enso / openLegalReviewReport` if you have `npm` in your PATH
as visible from SBT. 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 - Ensure that there are no more warnings, and if there are any go back to fix
the issues. 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 #### Additional Manual Considerations
The Scala Library notice contains the following mention: 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/canModify`](#executioncontextcanmodify)
- [`executionContext/receivesUpdates`](#executioncontextreceivesupdates) - [`executionContext/receivesUpdates`](#executioncontextreceivesupdates)
- [`search/receivesSuggestionsDatabaseUpdates`](#searchreceivessuggestionsdatabaseupdates) - [`search/receivesSuggestionsDatabaseUpdates`](#searchreceivessuggestionsdatabaseupdates)
- [Enables](#enables-4)
- [Disables](#disables-4)
- [File Management Operations](#file-management-operations) - [File Management Operations](#file-management-operations)
- [`file/write`](#filewrite) - [`file/write`](#filewrite)
- [`file/read`](#fileread) - [`file/read`](#fileread)
@ -94,6 +96,9 @@ transport formats, please look [here](./protocol-architecture).
- [`workspace/redo`](#workspaceredo) - [`workspace/redo`](#workspaceredo)
- [Monitoring](#monitoring) - [Monitoring](#monitoring)
- [`heartbeat/ping`](#heartbeatping) - [`heartbeat/ping`](#heartbeatping)
- [`heartbeat/init`](#heartbeatinit)
- [Refactoring](#refactoring)
- [`refactoring/renameProject`](#refactoringrenameproject)
- [Execution Management Operations](#execution-management-operations) - [Execution Management Operations](#execution-management-operations)
- [Execution Management Example](#execution-management-example) - [Execution Management Example](#execution-management-example)
- [Create Execution Context](#create-execution-context) - [Create Execution Context](#create-execution-context)
@ -113,9 +118,9 @@ transport formats, please look [here](./protocol-architecture).
- [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation) - [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation)
- [`executionContext/visualisationUpdate`](#executioncontextvisualisationupdate) - [`executionContext/visualisationUpdate`](#executioncontextvisualisationupdate)
- [Search Operations](#search-operations) - [Search Operations](#search-operations)
- [Suggestions Database Example](#suggestionsdatabaseexample) - [Suggestions Database Example](#suggestions-database-example)
- [`search/getSuggestionsDatabase`](#searchgetsuggestionsdatabase) - [`search/getSuggestionsDatabase`](#searchgetsuggestionsdatabase)
- [`search/invalidateSuggestionsDatabase`](#invalidatesuggestionsdatabase) - [`search/invalidateSuggestionsDatabase`](#searchinvalidatesuggestionsdatabase)
- [`search/getSuggestionsDatabaseVersion`](#searchgetsuggestionsdatabaseversion) - [`search/getSuggestionsDatabaseVersion`](#searchgetsuggestionsdatabaseversion)
- [`search/suggestionsDatabaseUpdate`](#searchsuggestionsdatabaseupdate) - [`search/suggestionsDatabaseUpdate`](#searchsuggestionsdatabaseupdate)
- [`search/completion`](#searchcompletion) - [`search/completion`](#searchcompletion)
@ -124,17 +129,17 @@ transport formats, please look [here](./protocol-architecture).
- [`io/redirectStandardOutput`](#ioredirectstdardoutput) - [`io/redirectStandardOutput`](#ioredirectstdardoutput)
- [`io/suppressStandardOutput`](#iosuppressstdardoutput) - [`io/suppressStandardOutput`](#iosuppressstdardoutput)
- [`io/standardOutputAppended`](#iostandardoutputappended) - [`io/standardOutputAppended`](#iostandardoutputappended)
- [`io/redirectStandardError`](#ioredirectstdarderror) - [`io/redirectStandardError`](#ioredirectstandarderror)
- [`io/suppressStandardError`](#iosuppressstdarderror) - [`io/suppressStandardError`](#iosuppressstandarderror)
- [`io/standardErrorAppended`](#iostandarderrorappended) - [`io/standardErrorAppended`](#iostandarderrorappended)
- [`io/feedStandardInput`](#iofeedstandardinput) - [`io/feedStandardInput`](#iofeedstandardinput)
- [`io/waitingForStandardInput`](#iowaitingforstandardinput) - [`io/waitingForStandardInput`](#iowaitingforstandardinput)
- [Errors](#errors) - [Errors](#errors-57)
- [`AccessDeniedError`](#accessdeniederror) - [`AccessDeniedError`](#accessdeniederror)
- [`FileSystemError`](#filesystemerror) - [`FileSystemError`](#filesystemerror)
- [`ContentRootNotFoundError`](#contentrootnotfounderror) - [`ContentRootNotFoundError`](#contentrootnotfounderror)
- [`FileNotFound`](#filenotfound) - [`FileNotFound`](#filenotfound)
- [`FileExists`](#fileexists-1) - [`FileExists`](#fileexists)
- [`OperationTimeoutError`](#operationtimeouterror) - [`OperationTimeoutError`](#operationtimeouterror)
- [`NotDirectory`](#notdirectory) - [`NotDirectory`](#notdirectory)
- [`StackItemNotFoundError`](#stackitemnotfounderror) - [`StackItemNotFoundError`](#stackitemnotfounderror)
@ -145,7 +150,6 @@ transport formats, please look [here](./protocol-architecture).
- [`VisualisationNotFoundError`](#visualisationnotfounderror) - [`VisualisationNotFoundError`](#visualisationnotfounderror)
- [`VisualisationExpressionError`](#visualisationexpressionerror) - [`VisualisationExpressionError`](#visualisationexpressionerror)
- [`VisualisationEvaluationError`](#visualisationevaluationerror) - [`VisualisationEvaluationError`](#visualisationevaluationerror)
- [`ExecutionFailedError`](#executionfailederror)
- [`FileNotOpenedError`](#filenotopenederror) - [`FileNotOpenedError`](#filenotopenederror)
- [`TextEditValidationError`](#texteditvalidationerror) - [`TextEditValidationError`](#texteditvalidationerror)
- [`InvalidVersionError`](#invalidversionerror) - [`InvalidVersionError`](#invalidversionerror)
@ -154,6 +158,8 @@ transport formats, please look [here](./protocol-architecture).
- [`SessionNotInitialisedError`](#sessionnotinitialisederror) - [`SessionNotInitialisedError`](#sessionnotinitialisederror)
- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror) - [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror)
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror) - [`SuggestionsDatabaseError`](#suggestionsdatabaseerror)
- [`ProjectNotFoundError`](#projectnotfounderror)
- [`ModuleNameNotResolvedError`](#modulenamenotresolvederror)
<!-- /MarkdownTOC --> <!-- /MarkdownTOC -->
@ -2085,6 +2091,33 @@ null;
None 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 ## Refactoring
The language server also provides refactoring operations to restructure an 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) - [`ProjectCloseError`](#projectcloseerror)
- [`LanguageServerError`](#languageservererror) - [`LanguageServerError`](#languageservererror)
- [`GlobalConfigurationAccessError`](#globalconfigurationaccesserror) - [`GlobalConfigurationAccessError`](#globalconfigurationaccesserror)
- [`ProjectCreateError`](#projectcreateerror)
- [`LoggingServiceUnavailable`](#loggingserviceunavailable) - [`LoggingServiceUnavailable`](#loggingserviceunavailable)
<!-- /MarkdownTOC --> <!-- /MarkdownTOC -->
@ -143,8 +144,8 @@ operation also includes spawning an instance of the language server open on the
specified project. specified project.
To open a project, an engine version that is specified in project settings needs To open a project, an engine version that is specified in project settings needs
to be installed. If `missingComponentAction` is set to `install` or to be installed. If `missingComponentAction` is set to `Install` or
`force-install-broken`, this action will install any missing components, `ForceInstallBroken`, this action will install any missing components,
otherwise, an error will be reported if a component is missing. A typical usage 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 scenario may consist of first trying to open the project without installing
missing components. If that fails with the `MissingComponentError`, the client missing components. If that fails with the `MissingComponentError`, the client
@ -165,7 +166,7 @@ interface ProjectOpenRequest {
/** /**
* Specifies how to handle missing components. * Specifies how to handle missing components.
* *
* If not provided, defaults to `fail`. * If not provided, defaults to `Fail`.
*/ */
missingComponentAction?: MissingComponentAction; missingComponentAction?: MissingComponentAction;
} }
@ -303,7 +304,7 @@ interface ProjectCreateRequest {
/** /**
* Specifies how to handle missing components. * Specifies how to handle missing components.
* *
* If not provided, defaults to `fail`. * If not provided, defaults to `Fail`.
*/ */
missingComponentAction?: MissingComponentAction; 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` ### `LoggingServiceUnavailable`
Signals that the logging service is not available. Signals that the logging service is not available.
```typescript ```typescript
"error" : { "error" : {
"code" : 4012, "code" : 4013,
"message" : "The logging service has failed to boot." "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, RuntimeShutdownResult,
ShutDownRuntime ShutDownRuntime
} }
import org.enso.loggingservice.LogLevel
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{Await, Future} 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. /** A lifecycle component used to start and stop a Language Server.
* *
* @param config a LS config * @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 extends LifecycleComponent
with LazyLogging { with LazyLogging {
@ -34,7 +36,7 @@ class LanguageServerComponent(config: LanguageServerConfig)
/** @inheritdoc */ /** @inheritdoc */
override def start(): Future[ComponentStarted.type] = { override def start(): Future[ComponentStarted.type] = {
logger.info("Starting Language Server...") logger.info("Starting Language Server...")
val module = new MainModule(config) val module = new MainModule(config, logLevel)
val initMainModule = val initMainModule =
for { for {
_ <- module.init _ <- module.init

View File

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

View File

@ -7,4 +7,6 @@ object InitializedEvent {
case object SuggestionsRepoInitialized extends InitializedEvent case object SuggestionsRepoInitialized extends InitializedEvent
case object FileVersionsRepoInitialized 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.InputOutputApi._
import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput} import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput}
import org.enso.languageserver.io.{InputOutputApi, InputOutputProtocol} 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.refactoring.RefactoringApi.RenameProject
import org.enso.languageserver.requesthandler._ import org.enso.languageserver.requesthandler._
import org.enso.languageserver.requesthandler.capability._ import org.enso.languageserver.requesthandler.capability._
import org.enso.languageserver.requesthandler.io._ 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.refactoring.RenameProjectHandler
import org.enso.languageserver.requesthandler.session.InitProtocolConnectionHandler import org.enso.languageserver.requesthandler.session.InitProtocolConnectionHandler
import org.enso.languageserver.requesthandler.text._ 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.ContextRegistryProtocol
import org.enso.languageserver.runtime.ExecutionApi._ import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.search.SearchApi.{
Completion,
GetSuggestionsDatabase,
GetSuggestionsDatabaseVersion,
Import,
InvalidateSuggestionsDatabase
}
import org.enso.languageserver.runtime.VisualisationApi.{ import org.enso.languageserver.runtime.VisualisationApi.{
AttachVisualisation, AttachVisualisation,
DetachVisualisation, DetachVisualisation,
ModifyVisualisation ModifyVisualisation
} }
import org.enso.languageserver.search.SearchApi._
import org.enso.languageserver.search.{SearchApi, SearchProtocol} import org.enso.languageserver.search.{SearchApi, SearchProtocol}
import org.enso.languageserver.session.JsonSession import org.enso.languageserver.session.JsonSession
import org.enso.languageserver.session.SessionApi.{ import org.enso.languageserver.session.SessionApi.{
@ -246,6 +243,7 @@ class JsonConnectionController(
), ),
requestTimeout requestTimeout
), ),
InitialPing -> InitialPingHandler.props,
AcquireCapability -> AcquireCapabilityHandler AcquireCapability -> AcquireCapabilityHandler
.props(capabilityRouter, requestTimeout, rpcSession), .props(capabilityRouter, requestTimeout, rpcSession),
ReleaseCapability -> ReleaseCapabilityHandler ReleaseCapability -> ReleaseCapabilityHandler

View File

@ -10,7 +10,7 @@ import org.enso.languageserver.capability.CapabilityApi.{
} }
import org.enso.languageserver.filemanager.FileManagerApi._ import org.enso.languageserver.filemanager.FileManagerApi._
import org.enso.languageserver.io.InputOutputApi._ 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.refactoring.RefactoringApi.RenameProject
import org.enso.languageserver.runtime.ExecutionApi._ import org.enso.languageserver.runtime.ExecutionApi._
import org.enso.languageserver.search.SearchApi._ import org.enso.languageserver.search.SearchApi._
@ -24,6 +24,7 @@ object JsonRpc {
*/ */
val protocol: Protocol = Protocol.empty val protocol: Protocol = Protocol.empty
.registerRequest(Ping) .registerRequest(Ping)
.registerRequest(InitialPing)
.registerRequest(InitProtocolConnection) .registerRequest(InitProtocolConnection)
.registerRequest(AcquireCapability) .registerRequest(AcquireCapability)
.registerRequest(ReleaseCapability) .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) .props(List(subsystem1.ref, subsystem2.ref, subsystem3.ref), 10.seconds)
) )
//when //when
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused) actorUnderTest ! Request(
MonitoringApi.Ping,
Number(1),
Unused
)
//then //then
subsystem1.expectMsg(Ping) subsystem1.expectMsg(Ping)
subsystem2.expectMsg(Ping) subsystem2.expectMsg(Ping)
@ -48,7 +52,11 @@ class PingHandlerSpec
.props(List(subsystem1.ref, subsystem2.ref, subsystem3.ref), 10.seconds) .props(List(subsystem1.ref, subsystem2.ref, subsystem3.ref), 10.seconds)
) )
//when //when
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused) actorUnderTest ! Request(
MonitoringApi.Ping,
Number(1),
Unused
)
subsystem1.expectMsg(Ping) subsystem1.expectMsg(Ping)
subsystem1.lastSender ! Pong subsystem1.lastSender ! Pong
subsystem2.expectMsg(Ping) subsystem2.expectMsg(Ping)
@ -56,7 +64,9 @@ class PingHandlerSpec
subsystem3.expectMsg(Ping) subsystem3.expectMsg(Ping)
subsystem3.lastSender ! Pong subsystem3.lastSender ! Pong
//then //then
expectMsg(ResponseResult(MonitoringApi.Ping, Number(1), Unused)) expectMsg(
ResponseResult(MonitoringApi.Ping, Number(1), Unused)
)
//teardown //teardown
system.stop(actorUnderTest) system.stop(actorUnderTest)
} }
@ -72,7 +82,11 @@ class PingHandlerSpec
) )
watch(actorUnderTest) watch(actorUnderTest)
//when //when
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused) actorUnderTest ! Request(
MonitoringApi.Ping,
Number(1),
Unused
)
subsystem2.expectMsg(Ping) subsystem2.expectMsg(Ping)
subsystem2.lastSender ! Pong subsystem2.lastSender ! Pong
subsystem3.expectMsg(Ping) subsystem3.expectMsg(Ping)

View File

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

View File

@ -8,13 +8,13 @@ import org.enso.languageserver.search.Suggestions
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json} import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
import org.enso.polyglot.data.Tree import org.enso.polyglot.data.Tree
import org.enso.polyglot.runtime.Runtime.Api 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 { "SuggestionsHandlerEvents" must {
"send suggestions database notifications" taggedAs Flaky in { "send suggestions database notifications" taggedAs Retry in {
val client = getInitialisedWsClient() val client = getInitialisedWsClient()
system.eventStream.publish(ProjectNameChangedEvent("Test", "Test")) system.eventStream.publish(ProjectNameChangedEvent("Test", "Test"))

View File

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

View File

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

View File

@ -72,7 +72,7 @@ class UpgradeSpec
* *
* If `launcherVersion` is not provided, the default one is used. * 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 * 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 * 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 * fail because the filesystem does not allow to access the executable as
@ -94,7 +94,7 @@ class UpgradeSpec
val root = launcherPath.getParent.getParent val root = launcherPath.getParent.getParent
FileSystem.writeTextFile(root / ".enso.portable", "mark") FileSystem.writeTextFile(root / ".enso.portable", "mark")
} }
Thread.sleep(100) Thread.sleep(250)
} }
/** Path to the launcher executable in the temporary distribution. /** Path to the launcher executable in the temporary distribution.
@ -156,7 +156,7 @@ class UpgradeSpec
} }
"upgrade" should { "upgrade" should {
"upgrade to latest version (excluding broken)" in { "upgrade to latest version (excluding broken)" taggedAs Retry in {
prepareDistribution( prepareDistribution(
portable = true, portable = true,
launcherVersion = Some(SemVer(0, 0, 2)) launcherVersion = Some(SemVer(0, 0, 2))
@ -166,7 +166,7 @@ class UpgradeSpec
checkVersion() shouldEqual SemVer(0, 0, 4) 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 // precondition for the test to make sense
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4) SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
@ -177,7 +177,7 @@ class UpgradeSpec
} }
"upgrade/downgrade to a specific version " + "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 // precondition for the test to make sense
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4) SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
@ -194,7 +194,7 @@ class UpgradeSpec
.trim shouldEqual "Test license" .trim shouldEqual "Test license"
} }
"upgrade also in installed mode" in { "upgrade also in installed mode" taggedAs Retry in {
prepareDistribution( prepareDistribution(
portable = false, portable = false,
launcherVersion = Some(SemVer(0, 0, 0)) launcherVersion = Some(SemVer(0, 0, 0))
@ -228,7 +228,9 @@ class UpgradeSpec
) )
checkVersion() shouldEqual SemVer(0, 0, 0) 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)
@ -260,6 +262,13 @@ class UpgradeSpec
.filter(_.startsWith("enso")) .filter(_.startsWith("enso"))
leftOverExecutables shouldEqual Seq(OS.executableName("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)
}
}
} }
"automatically trigger if an action requires a newer version and re-run " + "automatically trigger if an action requires a newer version and re-run " +
@ -271,8 +280,8 @@ class UpgradeSpec
val enginesPath = getTestDirectory / "enso" / "dist" val enginesPath = getTestDirectory / "enso" / "dist"
Files.createDirectories(enginesPath) Files.createDirectories(enginesPath)
// TODO [RW] re-enable this test when #1046 is done and the engine // TODO [RW] re-enable this test when #1046 or #1273 is done and the
// distribution can be used in the test // engine distribution can be used in the test
// FileSystem.copyDirectory( // FileSystem.copyDirectory(
// Path.of("target/distribution/"), // Path.of("target/distribution/"),
// enginesPath / "0.1.0" // 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, LanguageServerComponent,
LanguageServerConfig LanguageServerConfig
} }
import org.enso.loggingservice.LogLevel
import scala.concurrent.Await import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -16,10 +17,11 @@ object LanguageServerApp {
/** Runs a Language Server /** Runs a Language Server
* *
* @param config a config * @param config a config
* @param logLevel log level
*/ */
def run(config: LanguageServerConfig): Unit = { def run(config: LanguageServerConfig, logLevel: LogLevel): Unit = {
println("Starting Language Server...") println("Starting Language Server...")
val server = new LanguageServerComponent(config) val server = new LanguageServerComponent(config, logLevel)
Await.result(server.start(), 10.seconds) Await.result(server.start(), 10.seconds)
StdIn.readLine() StdIn.readLine()
Await.result(server.stop(), 10.seconds) Await.result(server.stop(), 10.seconds)

View File

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

View File

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

View File

@ -15,6 +15,7 @@ sealed abstract class LogLevel(final val level: Int) {
def shouldLog(other: LogLevel): Boolean = def shouldLog(other: LogLevel): Boolean =
other.level <= level other.level <= level
} }
object LogLevel { object LogLevel {
/** This log level should not be used by messages, instead it can be set as /** This log level should not be used by messages, instead it can be set as
@ -93,19 +94,38 @@ object LogLevel {
level.level.asJson 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]]. /** [[Decoder]] instance for [[LogLevel]].
*/ */
implicit val decoder: Decoder[LogLevel] = { json => implicit val decoder: Decoder[LogLevel] = { json =>
json.as[Int].flatMap { json.as[Int].flatMap { level =>
case Error.level => Right(Error) fromInteger(level).toRight(
case Warning.level => Right(Warning) DecodingFailure(s"`$level` is not a valid log level.", json.history)
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)
) )
} }
} }
/** 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 package org.enso.loggingservice
import org.enso.loggingservice.internal.service.{Client, Local, Server, Service} import org.enso.loggingservice.internal.service.{Client, Local, Server, Service}
import org.enso.loggingservice.internal.{ import org.enso.loggingservice.internal._
BlockingConsumerMessageQueue,
InternalLogMessage,
InternalLogger,
LoggerConnection
}
import org.enso.loggingservice.printers.{Printer, StderrPrinter} import org.enso.loggingservice.printers.{Printer, StderrPrinter}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
@ -14,9 +9,38 @@ import scala.concurrent.{ExecutionContext, Future}
/** Manages the logging service. /** Manages the logging service.
*/ */
object LoggingServiceManager { object LoggingServiceManager {
private val messageQueue = new BlockingConsumerMessageQueue() private val testLoggingPropertyKey = "org.enso.loggingservice.test-log-level"
private var currentService: Option[Service] = None
private var currentLevel: LogLevel = LogLevel.Trace 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 /** The default [[LoggerConnection]] that should be used by all backends which
* want to use the logging service. * want to use the logging service.
*/ */
@ -32,8 +56,6 @@ object LoggingServiceManager {
override def logLevel: LogLevel = currentLevel override def logLevel: LogLevel = currentLevel
} }
private var currentService: Option[Service] = None
/** Sets up the logging service, but in a separate thread to avoid stalling /** Sets up the logging service, but in a separate thread to avoid stalling
* the application. * 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.control.effect.{Async, ErrorChannel, Exec, Sync}
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
import org.enso.projectmanager.infrastructure.languageserver.{ import org.enso.projectmanager.infrastructure.languageserver.{
ExecutorWithUnlimitedPool,
LanguageServerGatewayImpl, LanguageServerGatewayImpl,
LanguageServerRegistry, LanguageServerRegistry,
ShutdownHookActivator ShutdownHookActivator
@ -25,6 +26,7 @@ import org.enso.projectmanager.service.config.GlobalConfigService
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService
import org.enso.projectmanager.service.{ import org.enso.projectmanager.service.{
MonadicProjectValidator, MonadicProjectValidator,
ProjectCreationService,
ProjectService, ProjectService,
ProjectServiceFailure, ProjectServiceFailure,
ValidationFailure ValidationFailure
@ -76,7 +78,9 @@ class MainModule[
config.network, config.network,
config.bootloader, config.bootloader,
config.supervision, config.supervision,
config.timeout config.timeout,
DefaultDistributionConfiguration,
ExecutorWithUnlimitedPool
), ),
"language-server-registry" "language-server-registry"
) )
@ -94,19 +98,25 @@ class MainModule[
config.timeout config.timeout
) )
lazy val projectCreationService =
new ProjectCreationService[F](DefaultDistributionConfiguration)
lazy val globalConfigService =
new GlobalConfigService[F](DefaultDistributionConfiguration)
lazy val projectService = lazy val projectService =
new ProjectService[F]( new ProjectService[F](
projectValidator, projectValidator,
projectRepository, projectRepository,
projectCreationService,
globalConfigService,
logging, logging,
clock, clock,
gen, gen,
languageServerGateway languageServerGateway,
DefaultDistributionConfiguration
) )
lazy val globalConfigService =
new GlobalConfigService[F](DefaultDistributionConfiguration)
lazy val runtimeVersionManagementService = lazy val runtimeVersionManagementService =
new RuntimeVersionManagementService[F](DefaultDistributionConfiguration) new RuntimeVersionManagementService[F](DefaultDistributionConfiguration)

View File

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

View File

@ -1,7 +1,10 @@
package org.enso.projectmanager.infrastructure.http package org.enso.projectmanager.infrastructure.http
import akka.actor.{Actor, ActorRef} 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. /** A fan-out receiver that delivers messages to multiple listeners.
*/ */
@ -10,7 +13,8 @@ class FanOutReceiver extends Actor {
override def receive: Receive = running() override def receive: Receive = running()
private def running(listeners: Set[ActorRef] = Set.empty): Receive = { 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) case msg => listeners.foreach(_ ! msg)
} }
@ -22,6 +26,6 @@ object FanOutReceiver {
* *
* @param listener a listener to attach * @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 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 { 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, HeartbeatTimeout,
SocketClosureTimeout 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 org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
@ -28,12 +31,21 @@ import scala.concurrent.duration.FiniteDuration
* @param timeout a session timeout * @param timeout a session timeout
* @param connectionFactory a web socket connection factory * @param connectionFactory a web socket connection factory
* @param scheduler a scheduler * @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( class HeartbeatSession(
socket: Socket, socket: Socket,
timeout: FiniteDuration, timeout: FiniteDuration,
connectionFactory: WebSocketConnectionFactory, connectionFactory: WebSocketConnectionFactory,
scheduler: Scheduler scheduler: Scheduler,
method: String,
sendConfirmations: Boolean,
quietErrors: Boolean
) extends Actor ) extends Actor
with ActorLogging with ActorLogging
with UnhandledLogging { with UnhandledLogging {
@ -57,7 +69,7 @@ class HeartbeatSession(
connection.send(s""" connection.send(s"""
|{ |{
| "jsonrpc": "2.0", | "jsonrpc": "2.0",
| "method": "heartbeat/ping", | "method": "$method",
| "id": "$requestId", | "id": "$requestId",
| "params": null | "params": null
|} |}
@ -66,7 +78,7 @@ class HeartbeatSession(
context.become(pongStage(cancellable)) context.become(pongStage(cancellable))
case WebSocketStreamFailure(th) => 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 context.parent ! ServerUnresponsive
stop() stop()
@ -81,16 +93,18 @@ class HeartbeatSession(
maybeJson match { maybeJson match {
case Left(error) => 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) => case Right(id) =>
if (id == requestId.toString) { if (id == requestId.toString) {
log.debug(s"Received correct pong message from $socket") log.debug(s"Received correct pong message from $socket")
if (sendConfirmations) {
context.parent ! HeartbeatReceived
}
cancellable.cancel() cancellable.cancel()
connection.disconnect() stop()
val closureTimeout =
scheduler.scheduleOnce(timeout, self, SocketClosureTimeout)
context.become(socketClosureStage(closureTimeout))
} else { } else {
log.warning(s"Received unknown response $payload") log.warning(s"Received unknown response $payload")
} }
@ -99,21 +113,17 @@ class HeartbeatSession(
case HeartbeatTimeout => case HeartbeatTimeout =>
log.debug(s"Heartbeat timeout detected for $requestId") log.debug(s"Heartbeat timeout detected for $requestId")
context.parent ! ServerUnresponsive context.parent ! ServerUnresponsive
connection.disconnect() stop()
val closureTimeout =
scheduler.scheduleOnce(timeout, self, SocketClosureTimeout)
context.become(socketClosureStage(closureTimeout))
case WebSocketStreamClosed => case WebSocketStreamClosed =>
context.parent ! ServerUnresponsive context.parent ! ServerUnresponsive
context.stop(self) context.stop(self)
case WebSocketStreamFailure(th) => 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 context.parent ! ServerUnresponsive
cancellable.cancel() cancellable.cancel()
connection.disconnect() stop()
context.stop(self)
case GracefulStop => case GracefulStop =>
cancellable.cancel() cancellable.cancel()
@ -126,13 +136,14 @@ class HeartbeatSession(
cancellable.cancel() cancellable.cancel()
case WebSocketStreamFailure(th) => 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) context.stop(self)
cancellable.cancel() cancellable.cancel()
case SocketClosureTimeout => case SocketClosureTimeout =>
log.error(s"Socket closure timed out") logError(s"Socket closure timed out")
context.stop(self) context.stop(self)
connection.detachListener(self)
case GracefulStop => // ignoring it, because the actor is already closing case GracefulStop => // ignoring it, because the actor is already closing
} }
@ -144,6 +155,22 @@ class HeartbeatSession(
context.become(socketClosureStage(closureTimeout)) 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 { object HeartbeatSession {
@ -156,7 +183,8 @@ object HeartbeatSession {
*/ */
case object SocketClosureTimeout 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 socket a server socket
* @param timeout a session timeout * @param timeout a session timeout
@ -170,6 +198,43 @@ object HeartbeatSession {
connectionFactory: WebSocketConnectionFactory, connectionFactory: WebSocketConnectionFactory,
scheduler: Scheduler scheduler: Scheduler
): Props = ): 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 package org.enso.projectmanager.infrastructure.languageserver
import akka.actor.Status.Failure import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import akka.actor.{Actor, ActorLogging, Props}
import akka.pattern.pipe
import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import org.enso.projectmanager.boot.configuration.BootloaderConfig import org.enso.projectmanager.boot.configuration.BootloaderConfig
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{
Boot,
FindFreeSocket,
ServerBootFailed, ServerBootFailed,
ServerBooted ServerBooted
} }
import org.enso.projectmanager.infrastructure.net.Tcp import org.enso.projectmanager.infrastructure.net.Tcp
import org.enso.projectmanager.util.UnhandledLogging import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration
/** It boots a Language Sever described by the `descriptor`. Upon boot failure /** It boots a Language Sever described by the `descriptor`. Upon boot failure
* looks up new available port and retries to boot the server. * 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 descriptor a LS descriptor
* @param config a bootloader config * @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( class LanguageServerBootLoader(
bootProgressTracker: ActorRef,
descriptor: LanguageServerDescriptor, descriptor: LanguageServerDescriptor,
config: BootloaderConfig config: BootloaderConfig,
bootTimeout: FiniteDuration,
executor: LanguageServerExecutor
) extends Actor ) extends Actor
with ActorLogging with ActorLogging
with UnhandledLogging { 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 import context.dispatcher
override def preStart(): Unit = { override def preStart(): Unit = {
@ -39,6 +56,10 @@ class LanguageServerBootLoader(
override def receive: Receive = findingSocket() 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 = { private def findingSocket(retry: Int = 0): Receive = {
case FindFreeSocket => case FindFreeSocket =>
log.debug("Looking for available socket to bind the language server") log.debug("Looking for available socket to bind the language server")
@ -53,53 +74,190 @@ class LanguageServerBootLoader(
s"binary:${descriptor.networkConfig.interface}:$binaryPort]" s"binary:${descriptor.networkConfig.interface}:$binaryPort]"
) )
self ! Boot self ! Boot
context.become(booting(jsonRpcPort, binaryPort, retry)) context.become(
bootingFirstTime(
rpcPort = jsonRpcPort,
dataPort = binaryPort,
retryCount = retry
)
)
case GracefulStop => case GracefulStop =>
context.stop(self) 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 => case Boot =>
log.debug("Booting a language server") log.debug("Booting a language server")
val config = LanguageServerConfig( context.actorOf(
descriptor.networkConfig.interface, LanguageServerProcess.props(
rpcPort, progressTracker = bootProgressTracker,
dataPort, descriptor = descriptor,
descriptor.rootId, bootTimeout = bootTimeout,
descriptor.root, rpcPort = rpcPort,
descriptor.name, dataPort = dataPort,
context.dispatcher executor = executor
),
s"process-wrapper-${descriptor.name}"
) )
val server = new LanguageServerComponent(config)
server.start().map(_ => config -> server) pipeTo self
case Failure(th) => case LanguageServerProcess.ServerTerminated(exitCode) =>
log.error( handleBootFailure(
th, shouldRetry,
s"An error occurred during boot of Language Server [${descriptor.name}]" retryCount,
bootRequester,
s"Language server terminated with exit code $exitCode before " +
s"finishing booting.",
None
) )
if (retryCount < config.numberOfRetries) {
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 context.system.scheduler
.scheduleOnce(config.delayBetweenRetry, self, FindFreeSocket) .scheduleOnce(config.delayBetweenRetry, self, FindFreeSocket)
context.become(findingSocket(retryCount + 1)) context.become(findingSocket(retryCount + 1))
} else { } else {
if (shouldRetry) {
log.error( log.error(
s"Tried $retryCount times to boot Language Server. Giving up." s"Tried $retryCount times to boot Language Server. Giving up."
) )
context.parent ! ServerBootFailed(th) } else {
log.error("Failed to restart the server. Giving up.")
}
bootRequester ! ServerBootFailed(
throwable.getOrElse(new RuntimeException(message))
)
context.stop(self) context.stop(self)
} }
}
case (config: LanguageServerConfig, server: LanguageServerComponent) => /** After successful boot, we cannot stop as it would stop our child process,
log.info(s"Language server booted [$config].") * so we just wait for it to terminate.
context.parent ! ServerBooted(config, server) *
* 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) context.stop(self)
case Restart =>
context.children.foreach(_ ! LanguageServerProcess.Stop)
context.become(
restartingWaitingForShutdown(connectionInfo, rebootRequester = sender())
)
case GracefulStop => 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 = private def findPort(): Int =
Tcp.findAvailablePort( Tcp.findAvailablePort(
descriptor.networkConfig.interface, descriptor.networkConfig.interface,
@ -107,29 +265,41 @@ class LanguageServerBootLoader(
descriptor.networkConfig.maxPort descriptor.networkConfig.maxPort
) )
private case object FindFreeSocket
private case object Boot
} }
object LanguageServerBootLoader { object LanguageServerBootLoader {
/** Creates a configuration object used to create a [[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 descriptor a LS descriptor
* @param config a bootloader config * @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 * @return a configuration object
*/ */
def props( def props(
bootProgressTracker: ActorRef,
descriptor: LanguageServerDescriptor, descriptor: LanguageServerDescriptor,
config: BootloaderConfig config: BootloaderConfig,
bootTimeout: FiniteDuration,
executor: LanguageServerExecutor
): Props = ): Props =
Props(new LanguageServerBootLoader(descriptor, config)) Props(
new LanguageServerBootLoader(
/** Find free socket command. bootProgressTracker,
*/ descriptor,
case object FindFreeSocket config,
bootTimeout,
/** Boot command. executor: LanguageServerExecutor
*/ )
case object Boot )
/** Signals that server boot failed. /** Signals that server boot failed.
* *
@ -139,12 +309,16 @@ object LanguageServerBootLoader {
/** Signals that server booted successfully. /** Signals that server booted successfully.
* *
* @param config a server config * @param connectionInfo a server config
* @param server a server lifecycle component * @param serverProcessManager an actor that manages the server process
* lifecycle, currently it is
* [[LanguageServerBootLoader]]
*/ */
case class ServerBooted( case class ServerBooted(
config: LanguageServerConfig, connectionInfo: LanguageServerConnectionInfo,
server: LanguageServerComponent 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 java.util.UUID
import akka.actor.Status.Failure
import akka.actor.{ import akka.actor.{
Actor, Actor,
ActorLogging, ActorLogging,
@ -14,12 +13,7 @@ import akka.actor.{
SupervisorStrategy, SupervisorStrategy,
Terminated Terminated
} }
import akka.pattern.pipe import nl.gn0s1s.bump.SemVer
import org.enso.languageserver.boot.LifecycleComponent.ComponentStopped
import org.enso.languageserver.boot.{
LanguageServerComponent,
LanguageServerConfig
}
import org.enso.projectmanager.boot.configuration.{ import org.enso.projectmanager.boot.configuration.{
BootloaderConfig, BootloaderConfig,
NetworkConfig, NetworkConfig,
@ -34,35 +28,38 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootL
ServerBootFailed, ServerBootFailed,
ServerBooted ServerBooted
} }
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.{ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController._
Boot,
BootTimeout,
ServerDied,
ShutDownServer,
ShutdownTimeout
}
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
import org.enso.projectmanager.model.Project import org.enso.projectmanager.model.Project
import org.enso.projectmanager.util.UnhandledLogging import org.enso.projectmanager.util.UnhandledLogging
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import scala.concurrent.duration._
/** A language server controller responsible for managing the server lifecycle. /** A language server controller responsible for managing the server lifecycle.
* It delegates all tasks to other actors like bootloader or supervisor. * It delegates all tasks to other actors like bootloader or supervisor.
* *
* @param project a project open by the server * @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 networkConfig a net config
* @param bootloaderConfig a bootloader config * @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config * @param supervisionConfig a supervision config
* @param timeoutConfig a timeout 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( class LanguageServerController(
project: Project, project: Project,
engineVersion: SemVer,
bootProgressTracker: ActorRef,
networkConfig: NetworkConfig, networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig, bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig, supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
) extends Actor ) extends Actor
with ActorLogging with ActorLogging
with Stash with Stash
@ -74,8 +71,12 @@ class LanguageServerController(
LanguageServerDescriptor( LanguageServerDescriptor(
name = s"language-server-${project.id}", name = s"language-server-${project.id}",
rootId = UUID.randomUUID(), rootId = UUID.randomUUID(),
root = project.path.get, rootPath = project.path.get,
networkConfig = networkConfig networkConfig = networkConfig,
distributionConfiguration = distributionConfiguration,
engineVersion = engineVersion,
jvmSettings = distributionConfiguration.defaultJVMSettings,
discardOutput = distributionConfiguration.shouldDiscardChildOutput
) )
override def supervisorStrategy: SupervisorStrategy = override def supervisorStrategy: SupervisorStrategy =
@ -92,21 +93,23 @@ class LanguageServerController(
case Boot => case Boot =>
val bootloader = val bootloader =
context.actorOf( context.actorOf(
LanguageServerBootLoader.props(descriptor, bootloaderConfig), LanguageServerBootLoader
"bootloader" .props(
bootProgressTracker,
descriptor,
bootloaderConfig,
timeoutConfig.bootTimeout,
executor
),
s"bootloader-${descriptor.name}"
) )
context.watch(bootloader) context.watch(bootloader)
val timeoutCancellable = context.become(booting(bootloader))
context.system.scheduler.scheduleOnce(30.seconds, self, BootTimeout)
context.become(booting(bootloader, timeoutCancellable))
case _ => stash() case _ => stash()
} }
private def booting( private def booting(Bootloader: ActorRef): Receive = {
Bootloader: ActorRef,
timeoutCancellable: Cancellable
): Receive = {
case BootTimeout => case BootTimeout =>
log.error(s"Booting failed for $descriptor") log.error(s"Booting failed for $descriptor")
unstashAll() unstashAll()
@ -115,28 +118,25 @@ class LanguageServerController(
case ServerBootFailed(th) => case ServerBootFailed(th) =>
log.error(th, s"Booting failed for $descriptor") log.error(th, s"Booting failed for $descriptor")
unstashAll() unstashAll()
timeoutCancellable.cancel()
context.become(bootFailed(LanguageServerProtocol.ServerBootFailed(th))) context.become(bootFailed(LanguageServerProtocol.ServerBootFailed(th)))
case ServerBooted(config, server) => case ServerBooted(connectionInfo, serverProcessManager) =>
unstashAll() unstashAll()
timeoutCancellable.cancel() context.become(supervising(connectionInfo, serverProcessManager))
context.become(supervising(config, server))
context.actorOf( context.actorOf(
LanguageServerSupervisor.props( LanguageServerSupervisor.props(
config, connectionInfo,
server, serverProcessManager,
supervisionConfig, supervisionConfig,
new AkkaBasedWebSocketConnectionFactory(), new AkkaBasedWebSocketConnectionFactory(),
context.system.scheduler context.system.scheduler
), ),
"supervisor" s"supervisor-${descriptor.name}"
) )
case Terminated(Bootloader) => case Terminated(Bootloader) =>
log.error(s"Bootloader for project ${project.name} failed") log.error(s"Bootloader for project ${project.name} failed")
unstashAll() unstashAll()
timeoutCancellable.cancel()
context.become( context.become(
bootFailed( bootFailed(
LanguageServerProtocol.ServerBootFailed( LanguageServerProtocol.ServerBootFailed(
@ -149,33 +149,57 @@ class LanguageServerController(
} }
private def supervising( private def supervising(
config: LanguageServerConfig, connectionInfo: LanguageServerConnectionInfo,
server: LanguageServerComponent, serverProcessManager: ActorRef,
clients: Set[UUID] = Set.empty clients: Set[UUID] = Set.empty
): Receive = { ): Receive = {
case StartServer(clientId, _) => 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."
)
)
} else {
sender() ! ServerStarted( sender() ! ServerStarted(
LanguageServerSockets( LanguageServerSockets(
Socket(config.interface, config.rpcPort), Socket(connectionInfo.interface, connectionInfo.rpcPort),
Socket(config.interface, config.dataPort) Socket(connectionInfo.interface, connectionInfo.dataPort)
) )
) )
context.become(supervising(config, server, clients + clientId)) context.become(
supervising(connectionInfo, serverProcessManager, clients + clientId)
)
}
case Terminated(_) => case Terminated(_) =>
log.debug(s"Bootloader for $project terminated.") log.debug(s"Bootloader for $project terminated.")
case StopServer(clientId, _) => case StopServer(clientId, _) =>
removeClient(config, server, clients, clientId, Some(sender())) removeClient(
connectionInfo,
serverProcessManager,
clients,
clientId,
Some(sender())
)
case ShutDownServer => case ShutDownServer =>
shutDownServer(server, None) shutDownServer(None)
case ClientDisconnected(clientId) => case ClientDisconnected(clientId) =>
removeClient(config, server, clients, clientId, None) removeClient(
connectionInfo,
serverProcessManager,
clients,
clientId,
None
)
case RenameProject(_, oldName, newName) => case RenameProject(_, oldName, newName) =>
val socket = Socket(config.interface, config.rpcPort) val socket = Socket(connectionInfo.interface, connectionInfo.rpcPort)
context.actorOf( context.actorOf(
ProjectRenameAction ProjectRenameAction
.props( .props(
@ -191,33 +215,31 @@ class LanguageServerController(
) )
case ServerDied => case ServerDied =>
log.error(s"Language server died [$config]") log.error(s"Language server died [$connectionInfo]")
context.stop(self) context.stop(self)
} }
private def removeClient( private def removeClient(
config: LanguageServerConfig, connectionInfo: LanguageServerConnectionInfo,
server: LanguageServerComponent, serverProcessManager: ActorRef,
clients: Set[UUID], clients: Set[UUID],
clientId: UUID, clientId: UUID,
maybeRequester: Option[ActorRef] maybeRequester: Option[ActorRef]
): Unit = { ): Unit = {
val updatedClients = clients - clientId val updatedClients = clients - clientId
if (updatedClients.isEmpty) { if (updatedClients.isEmpty) {
shutDownServer(server, maybeRequester) shutDownServer(maybeRequester)
} else { } else {
sender() ! CannotDisconnectOtherClients sender() ! CannotDisconnectOtherClients
context.become(supervising(config, server, updatedClients)) context.become(
supervising(connectionInfo, serverProcessManager, updatedClients)
)
} }
} }
private def shutDownServer( private def shutDownServer(maybeRequester: Option[ActorRef]): Unit = {
server: LanguageServerComponent,
maybeRequester: Option[ActorRef]
): Unit = {
log.debug(s"Shutting down a language server for project ${project.id}") log.debug(s"Shutting down a language server for project ${project.id}")
context.children.foreach(_ ! GracefulStop) context.children.foreach(_ ! GracefulStop)
server.stop() pipeTo self
val cancellable = val cancellable =
context.system.scheduler context.system.scheduler
.scheduleOnce(timeoutConfig.shutdownTimeout, self, ShutdownTimeout) .scheduleOnce(timeoutConfig.shutdownTimeout, self, ShutdownTimeout)
@ -225,7 +247,7 @@ class LanguageServerController(
} }
private def bootFailed(failure: ServerStartupFailure): Receive = { private def bootFailed(failure: ServerStartupFailure): Receive = {
case StartServer(_, _) => case StartServer(_, _, _, _) =>
sender() ! failure sender() ! failure
stop() stop()
} }
@ -234,18 +256,16 @@ class LanguageServerController(
cancellable: Cancellable, cancellable: Cancellable,
maybeRequester: Option[ActorRef] maybeRequester: Option[ActorRef]
): Receive = { ): 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() cancellable.cancel()
if (exitCode == 0) {
log.info(s"Language server shut down successfully [$project].") 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) maybeRequester.foreach(_ ! ServerStopped)
stop() stop()
@ -254,7 +274,7 @@ class LanguageServerController(
maybeRequester.foreach(_ ! ServerShutdownTimedOut) maybeRequester.foreach(_ ! ServerShutdownTimedOut)
stop() stop()
case StartServer(_, _) => case StartServer(_, _, _, _) =>
sender() ! PreviousInstanceNotShutDown sender() ! PreviousInstanceNotShutDown
} }
@ -283,26 +303,40 @@ object LanguageServerController {
/** Creates a configuration object used to create a [[LanguageServerController]]. /** Creates a configuration object used to create a [[LanguageServerController]].
* *
* @param project a project open by the server * @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 networkConfig a net config
* @param bootloaderConfig a bootloader config * @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config * @param supervisionConfig a supervision config
* @param timeoutConfig a timeout 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 * @return a configuration object
*/ */
def props( def props(
project: Project, project: Project,
engineVersion: SemVer,
bootProgressTracker: ActorRef,
networkConfig: NetworkConfig, networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig, bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig, supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
): Props = ): Props =
Props( Props(
new LanguageServerController( new LanguageServerController(
project, project,
engineVersion,
bootProgressTracker,
networkConfig, networkConfig,
bootloaderConfig, bootloaderConfig,
supervisionConfig, supervisionConfig,
timeoutConfig timeoutConfig,
distributionConfiguration,
executor
) )
) )

View File

@ -2,18 +2,32 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.util.UUID import java.util.UUID
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.boot.configuration.NetworkConfig 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 name a name of the LS
* @param rootId a content root id * @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 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( case class LanguageServerDescriptor(
name: String, name: String,
rootId: UUID, rootId: UUID,
root: String, rootPath: String,
networkConfig: NetworkConfig 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 java.util.UUID
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.LanguageServerSockets import org.enso.projectmanager.data.LanguageServerSockets
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol.{ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol.{
CheckTimeout, CheckTimeout,
@ -20,13 +22,20 @@ trait LanguageServerGateway[F[+_, +_]] {
/** Starts a language server. /** 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 clientId a requester id
* @param project a project to start * @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 * @return either a failure or sockets that a language server listens on
*/ */
def start( def start(
progressTracker: ActorRef,
clientId: UUID, clientId: UUID,
project: Project project: Project,
version: SemVer
): F[ServerStartupFailure, LanguageServerSockets] ): F[ServerStartupFailure, LanguageServerSockets]
/** Stops a lang. server. /** Stops a lang. server.

View File

@ -5,6 +5,7 @@ import java.util.UUID
import akka.actor.{ActorRef, ActorSystem} import akka.actor.{ActorRef, ActorSystem}
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.boot.configuration.TimeoutConfig import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.core.CovariantFlatMap import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._ import org.enso.projectmanager.control.core.syntax._
@ -38,14 +39,23 @@ class LanguageServerGatewayImpl[
/** @inheritdoc */ /** @inheritdoc */
override def start( override def start(
progressTracker: ActorRef,
clientId: UUID, clientId: UUID,
project: Project project: Project,
version: SemVer
): F[ServerStartupFailure, LanguageServerSockets] = { ): 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] Async[F]
.fromFuture { () => .fromFuture { () =>
(registry ? StartServer(clientId, project)).mapTo[ServerStartupResult] (registry ? StartServer(
clientId,
project,
version,
progressTracker
)).mapTo[ServerStartupResult]
} }
.mapError(_ => ServerBootTimedOut) .mapError(_ => ServerBootTimedOut)
.flatMap { .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 java.util.UUID
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.LanguageServerSockets import org.enso.projectmanager.data.LanguageServerSockets
import org.enso.projectmanager.model.Project import org.enso.projectmanager.model.Project
@ -13,8 +15,16 @@ object LanguageServerProtocol {
* *
* @param clientId the requester id * @param clientId the requester id
* @param project the project to start * @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. /** 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.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
import org.enso.projectmanager.util.UnhandledLogging import org.enso.projectmanager.util.UnhandledLogging
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
/** An actor that routes request regarding lang. server lifecycle to the /** An actor that routes request regarding lang. server lifecycle to the
* right controller that manages the server. * 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 networkConfig a net config
* @param bootloaderConfig a bootloader config * @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config * @param supervisionConfig a supervision config
* @param timeoutConfig a timeout 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( class LanguageServerRegistry(
networkConfig: NetworkConfig, networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig, bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig, supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
) extends Actor ) extends Actor
with ActorLogging with ActorLogging
with UnhandledLogging { with UnhandledLogging {
@ -44,7 +50,7 @@ class LanguageServerRegistry(
private def running( private def running(
serverControllers: Map[UUID, ActorRef] = Map.empty serverControllers: Map[UUID, ActorRef] = Map.empty
): Receive = { ): Receive = {
case msg @ StartServer(_, project) => case msg @ StartServer(_, project, engineVersion, progressTracker) =>
if (serverControllers.contains(project.id)) { if (serverControllers.contains(project.id)) {
serverControllers(project.id).forward(msg) serverControllers(project.id).forward(msg)
} else { } else {
@ -52,10 +58,14 @@ class LanguageServerRegistry(
LanguageServerController LanguageServerController
.props( .props(
project, project,
engineVersion,
progressTracker,
networkConfig, networkConfig,
bootloaderConfig, bootloaderConfig,
supervisionConfig, supervisionConfig,
timeoutConfig timeoutConfig,
distributionConfiguration,
executor
), ),
s"language-server-controller-${project.id}" s"language-server-controller-${project.id}"
) )
@ -116,20 +126,27 @@ object LanguageServerRegistry {
* @param bootloaderConfig a bootloader config * @param bootloaderConfig a bootloader config
* @param supervisionConfig a supervision config * @param supervisionConfig a supervision config
* @param timeoutConfig a timeout 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( def props(
networkConfig: NetworkConfig, networkConfig: NetworkConfig,
bootloaderConfig: BootloaderConfig, bootloaderConfig: BootloaderConfig,
supervisionConfig: SupervisionConfig, supervisionConfig: SupervisionConfig,
timeoutConfig: TimeoutConfig timeoutConfig: TimeoutConfig,
distributionConfiguration: DistributionConfiguration,
executor: LanguageServerExecutor
): Props = ): Props =
Props( Props(
new LanguageServerRegistry( new LanguageServerRegistry(
networkConfig, networkConfig,
bootloaderConfig, bootloaderConfig,
supervisionConfig, supervisionConfig,
timeoutConfig timeoutConfig,
distributionConfiguration,
executor
) )
) )

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package org.enso.projectmanager.infrastructure.repository package org.enso.projectmanager.infrastructure.repository
import java.io.File import java.io.File
import java.nio.file.Path
import java.util.UUID import java.util.UUID
import org.enso.projectmanager.model.Project import org.enso.projectmanager.model.Project
@ -18,12 +19,15 @@ trait ProjectRepository[F[+_, +_]] {
*/ */
def exists(name: String): F[ProjectRepositoryFailure, Boolean] 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 * If it was not set, a new path is generated for it. Otherwise, the function
* @return * 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. /** 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.time.OffsetDateTime
import java.util.UUID import java.util.UUID
import org.enso.pkg.{DefaultEnsoVersion, EnsoVersion}
/** Project entity. /** Project entity.
* *
* @param id a project id * @param id a project id
* @param name a project name * @param name a project name
* @param kind a project kind * @param kind a project kind
* @param created a project creation time * @param created a project creation time
* @param engineVersion version of the engine associated with the project
* @param lastOpened a project last open time * @param lastOpened a project last open time
* @param path a path to the project structure * @param path a path to the project structure
*/ */
@ -17,6 +20,7 @@ case class Project(
name: String, name: String,
kind: ProjectKind, kind: ProjectKind,
created: OffsetDateTime, created: OffsetDateTime,
engineVersion: EnsoVersion = DefaultEnsoVersion,
lastOpened: Option[OffsetDateTime] = None, lastOpened: Option[OffsetDateTime] = None,
path: Option[String] = 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.jsonrpc.{JsonRpcServer, MessageHandler, Method, Request}
import org.enso.projectmanager.boot.configuration.TimeoutConfig import org.enso.projectmanager.boot.configuration.TimeoutConfig
import org.enso.projectmanager.control.core.CovariantFlatMap 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.{ import org.enso.projectmanager.event.ClientEvent.{
ClientConnected, ClientConnected,
ClientDisconnected ClientDisconnected
@ -30,7 +30,7 @@ import scala.concurrent.duration._
* @param runtimeVersionManagementService version management service * @param runtimeVersionManagementService version management service
* @param timeoutConfig a request timeout config * @param timeoutConfig a request timeout config
*/ */
class ClientController[F[+_, +_]: Exec: CovariantFlatMap]( class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
clientId: UUID, clientId: UUID,
projectService: ProjectServiceApi[F], projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F], globalConfigService: GlobalConfigServiceApi[F],
@ -44,11 +44,19 @@ class ClientController[F[+_, +_]: Exec: CovariantFlatMap](
private val requestHandlers: Map[Method, Props] = private val requestHandlers: Map[Method, Props] =
Map( Map(
ProjectCreate -> ProjectCreateHandler ProjectCreate -> ProjectCreateHandler
.props[F](projectService, timeoutConfig.requestTimeout), .props[F](
globalConfigService,
projectService,
timeoutConfig.requestTimeout
),
ProjectDelete -> ProjectDeleteHandler ProjectDelete -> ProjectDeleteHandler
.props[F](projectService, timeoutConfig.requestTimeout), .props[F](projectService, timeoutConfig.requestTimeout),
ProjectOpen -> ProjectOpenHandler ProjectOpen -> ProjectOpenHandler
.props[F](clientId, projectService, timeoutConfig.bootTimeout), .props[F](
clientId,
projectService,
timeoutConfig.bootTimeout
),
ProjectClose -> ProjectCloseHandler ProjectClose -> ProjectCloseHandler
.props[F]( .props[F](
clientId, clientId,
@ -118,7 +126,7 @@ object ClientController {
* @param timeoutConfig a request timeout config * @param timeoutConfig a request timeout config
* @return a configuration object * @return a configuration object
*/ */
def props[F[+_, +_]: Exec: CovariantFlatMap]( def props[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
clientId: UUID, clientId: UUID,
projectService: ProjectServiceApi[F], projectService: ProjectServiceApi[F],
globalConfigService: GlobalConfigServiceApi[F], globalConfigService: GlobalConfigServiceApi[F],

View File

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

View File

@ -321,4 +321,8 @@ object ProjectManagementApi {
case class GlobalConfigurationAccessError(msg: String) case class GlobalConfigurationAccessError(msg: String)
extends Error(4011, msg) 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 */ /** @inheritdoc */
override def handleRequest = { params => override def handleRequest = { params =>
val progressTracker = sender()
for { for {
_ <- service.installEngine( _ <- service.installEngine(
progressTracker, progressTracker = self,
params.version, version = params.version,
params.forceInstallBroken.getOrElse(false) forceInstallBroken = params.forceInstallBroken.getOrElse(false)
) )
} yield Unused } yield Unused
} }

View File

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

View File

@ -1,109 +1,86 @@
package org.enso.projectmanager.requesthandler package org.enso.projectmanager.requesthandler
import java.util.UUID
import akka.actor._ 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.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.data.MissingComponentAction
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectCreate 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.{ import org.enso.projectmanager.service.{
ProjectServiceApi, ProjectServiceApi,
ProjectServiceFailure ProjectServiceFailure
} }
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
/** A request handler for `project/create` commands. /** 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 * @param requestTimeout a request timeout
*/ */
class ProjectCreateHandler[F[+_, +_]: Exec]( class ProjectCreateHandler[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
service: ProjectServiceApi[F], configurationService: GlobalConfigServiceApi[F],
projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration requestTimeout: FiniteDuration
) extends Actor ) extends RequestHandler[
with ActorLogging F,
with UnhandledLogging { ProjectServiceFailure,
override def receive: Receive = requestStage ProjectCreate.type,
ProjectCreate.Params,
ProjectCreate.Result
](
ProjectCreate,
Some(requestTimeout)
) {
import context.dispatcher override def handleRequest = { params =>
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 version = params.version.getOrElse(DefaultEnsoVersion)
val missingComponentAction = val missingComponentAction =
params.missingComponentAction.getOrElse(MissingComponentAction.Fail) params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
Exec[F]
.exec( for {
service actualVersion <- configurationService
.createUserProject(params.name, version, missingComponentAction) .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))
} }
} projectId <- projectService.createUserProject(
progressTracker = self,
private def responseStage( name = params.name,
id: Id, engineVersion = actualVersion,
replyTo: ActorRef, missingComponentAction = missingComponentAction
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)
) )
cancellable.cancel() } yield ProjectCreate.Result(projectId)
context.stop(self)
} }
} }
object ProjectCreateHandler { object ProjectCreateHandler {
/** Creates a configuration object used to create a [[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 * @param requestTimeout a request timeout
* @return a configuration object * @return a configuration object
*/ */
def props[F[+_, +_]: Exec]( def props[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
service: ProjectServiceApi[F], configurationService: GlobalConfigServiceApi[F],
projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration requestTimeout: FiniteDuration
): Props = ): 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 java.util.UUID
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status} import akka.actor.Props
import akka.pattern.pipe import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.jsonrpc.Errors.ServiceError import org.enso.projectmanager.control.core.syntax._
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import org.enso.projectmanager.control.effect.Exec import org.enso.projectmanager.control.effect.Exec
import org.enso.projectmanager.data.{ import org.enso.projectmanager.data.MissingComponentAction
LanguageServerSockets,
MissingComponentAction
}
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectOpen 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.{ import org.enso.projectmanager.service.{
ProjectServiceApi, ProjectServiceApi,
ProjectServiceFailure ProjectServiceFailure
} }
import org.enso.projectmanager.util.UnhandledLogging
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
/** A request handler for `project/open` commands. /** A request handler for `project/open` commands.
* *
* @param clientId the requester id * @param clientId the requester id
* @param service a project service * @param projectService a project service
* @param requestTimeout a request timeout * @param requestTimeout a request timeout
*/ */
class ProjectOpenHandler[F[+_, +_]: Exec]( class ProjectOpenHandler[F[+_, +_]: Exec: CovariantFlatMap](
clientId: UUID, clientId: UUID,
service: ProjectServiceApi[F], projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration requestTimeout: FiniteDuration
) extends Actor ) extends RequestHandler[
with ActorLogging F,
with UnhandledLogging { ProjectServiceFailure,
override def receive: Receive = requestStage 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 =>
private def requestStage: Receive = {
case Request(ProjectOpen, id, params: ProjectOpen.Params) =>
val missingComponentAction = val missingComponentAction =
params.missingComponentAction.getOrElse(MissingComponentAction.Fail) params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
Exec[F]
.exec( for {
service sockets <- projectService.openProject(
.openProject(clientId, params.projectId, missingComponentAction) progressTracker = self,
clientId = clientId,
projectId = params.projectId,
missingComponentAction = missingComponentAction
) )
.pipeTo(self) } yield ProjectOpen.Result(
val cancellable = languageServerJsonAddress = sockets.jsonSocket,
context.system.scheduler languageServerBinaryAddress = sockets.binarySocket
.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)
) )
cancellable.cancel()
context.stop(self)
} }
} }
@ -93,15 +64,21 @@ object ProjectOpenHandler {
/** Creates a configuration object used to create a [[ProjectOpenHandler]]. /** Creates a configuration object used to create a [[ProjectOpenHandler]].
* *
* @param clientId the requester id * @param clientId the requester id
* @param service a project service * @param projectService a project service
* @param requestTimeout a request timeout * @param requestTimeout a request timeout
* @return a configuration object * @return a configuration object
*/ */
def props[F[+_, +_]: Exec]( def props[F[+_, +_]: Exec: CovariantFlatMap](
clientId: UUID, clientId: UUID,
service: ProjectServiceApi[F], projectService: ProjectServiceApi[F],
requestTimeout: FiniteDuration requestTimeout: FiniteDuration
): Props = ): 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 DataStoreFailure(msg) => ProjectDataStoreError(msg)
case ProjectExists => ProjectExistsError case ProjectExists => ProjectExistsError
case ProjectNotFound => ProjectNotFoundError case ProjectNotFound => ProjectNotFoundError
case ProjectCreateFailed(msg) => ProjectCreateError(msg)
case ProjectOpenFailed(msg) => ProjectOpenError(msg) case ProjectOpenFailed(msg) => ProjectOpenError(msg)
case ProjectCloseFailed(msg) => ProjectCloseError(msg) case ProjectCloseFailed(msg) => ProjectCloseError(msg)
case ProjectNotOpen => ProjectNotOpenError case ProjectNotOpen => ProjectNotOpenError

View File

@ -61,7 +61,7 @@ abstract class RequestHandler[
.exec(result) .exec(result)
.map(_.map(ResponseResult(method, request.id, _))) .map(_.map(ResponseResult(method, request.id, _)))
.pipeTo(self) .pipeTo(self)
val cancellable = { val timeoutCancellable = {
requestTimeout.map { timeout => requestTimeout.map { timeout =>
context.system.scheduler.scheduleOnce( context.system.scheduler.scheduleOnce(
timeout, 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. /** Defines the actual logic for handling the request.
@ -86,12 +86,12 @@ abstract class RequestHandler[
private def responseStage( private def responseStage(
id: Id, id: Id,
replyTo: ActorRef, replyTo: ActorRef,
cancellable: Option[Cancellable] timeoutCancellable: Option[Cancellable]
): Receive = { ): Receive = {
case Status.Failure(ex) => case Status.Failure(ex) =>
log.error(ex, s"Failure during $method operation:") log.error(ex, s"Failure during $method operation:")
replyTo ! ResponseError(Some(id), ServiceError) replyTo ! ResponseError(Some(id), ServiceError)
cancellable.foreach(_.cancel()) timeoutCancellable.foreach(_.cancel())
context.stop(self) context.stop(self)
case RequestTimeout => case RequestTimeout =>
@ -103,15 +103,34 @@ abstract class RequestHandler[
log.error(s"Request $id failed due to $failure") log.error(s"Request $id failed due to $failure")
val error = implicitly[FailureMapper[FailureType]].mapFailure(failure) val error = implicitly[FailureMapper[FailureType]].mapFailure(failure)
replyTo ! ResponseError(Some(id), error) replyTo ! ResponseError(Some(id), error)
cancellable.foreach(_.cancel()) timeoutCancellable.foreach(_.cancel())
context.stop(self) context.stop(self)
case Right(response) => case Right(response) =>
replyTo ! response replyTo ! response
cancellable.foreach(_.cancel()) timeoutCancellable.foreach(_.cancel())
context.stop(self) context.stop(self)
case notification: ProgressNotification => case notification: ProgressNotification =>
notification match {
case ProgressNotification.TaskStarted(_, _, _) =>
abandonTimeout(id, replyTo, timeoutCancellable)
case _ =>
}
replyTo ! translateProgressNotification(method.name, notification) 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 java.util.UUID
import akka.actor.ActorRef
import cats.MonadError 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.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._ 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.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.data.{ import org.enso.projectmanager.data.{
LanguageServerSockets, LanguageServerSockets,
MissingComponentAction, MissingComponentAction,
ProjectMetadata ProjectMetadata
} }
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerGateway 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.log.Logging
import org.enso.projectmanager.infrastructure.random.Generator import org.enso.projectmanager.infrastructure.random.Generator
import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailure.{ import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailure.{
@ -35,11 +37,18 @@ import org.enso.projectmanager.service.ValidationFailure.{
EmptyName, EmptyName,
NameContainsForbiddenCharacter 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. /** Implementation of business logic for project management.
* *
* @param validator a project validator * @param validator a project validator
* @param repo a project repository * @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 log a logging facility
* @param clock a clock * @param clock a clock
* @param gen a random generator * @param gen a random generator
@ -47,10 +56,13 @@ import org.enso.projectmanager.service.ValidationFailure.{
class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync]( class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
validator: ProjectValidator[F], validator: ProjectValidator[F],
repo: ProjectRepository[F], repo: ProjectRepository[F],
projectCreationService: ProjectCreationServiceApi[F],
configurationService: GlobalConfigServiceApi[F],
log: Logging[F], log: Logging[F],
clock: Clock[F], clock: Clock[F],
gen: Generator[F], gen: Generator[F],
languageServerGateway: LanguageServerGateway[F] languageServerGateway: LanguageServerGateway[F],
distributionConfiguration: DistributionConfiguration
)(implicit E: MonadError[F[ProjectServiceFailure, *], ProjectServiceFailure]) )(implicit E: MonadError[F[ProjectServiceFailure, *], ProjectServiceFailure])
extends ProjectServiceApi[F] { extends ProjectServiceApi[F] {
@ -58,25 +70,30 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
/** @inheritdoc */ /** @inheritdoc */
override def createUserProject( override def createUserProject(
progressTracker: ActorRef,
name: String, name: String,
version: EnsoVersion, engineVersion: SemVer,
missingComponentAction: MissingComponentAction missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, UUID] = { ): F[ProjectServiceFailure, UUID] = for {
// TODO [RW] new component handling
val _ = (version, missingComponentAction)
// format: off
for {
projectId <- gen.randomUUID() projectId <- gen.randomUUID()
_ <- log.debug(s"Creating project $name $projectId.") _ <- log.debug(s"Creating project $name $projectId.")
_ <- validateName(name) _ <- validateName(name)
_ <- checkIfNameExists(name) _ <- checkIfNameExists(name)
creationTime <- clock.nowInUtc() creationTime <- clock.nowInUtc()
project = Project(projectId, name, UserProject, creationTime) project = Project(projectId, name, UserProject, creationTime)
_ <- repo.create(project).mapError(toServiceFailure) 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.") _ <- log.info(s"Project $project created.")
} yield projectId } yield projectId
// format: on
}
/** @inheritdoc */ /** @inheritdoc */
override def deleteUserProject( override def deleteUserProject(
@ -176,12 +193,11 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
/** @inheritdoc */ /** @inheritdoc */
override def openProject( override def openProject(
progressTracker: ActorRef,
clientId: UUID, clientId: UUID,
projectId: UUID, projectId: UUID,
missingComponentAction: MissingComponentAction missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, LanguageServerSockets] = { ): F[ProjectServiceFailure, LanguageServerSockets] = {
// TODO [RW] new component handling
val _ = missingComponentAction
// format: off // format: off
for { for {
_ <- log.debug(s"Opening project $projectId") _ <- log.debug(s"Opening project $projectId")
@ -189,17 +205,46 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
openTime <- clock.nowInUtc() openTime <- clock.nowInUtc()
updated = project.copy(lastOpened = Some(openTime)) updated = project.copy(lastOpened = Some(openTime))
_ <- repo.update(updated).mapError(toServiceFailure) _ <- repo.update(updated).mapError(toServiceFailure)
sockets <- startServer(clientId, updated) sockets <- startServer(progressTracker, clientId, updated, missingComponentAction)
} yield sockets } yield sockets
// format: on // 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( private def startServer(
progressTracker: ActorRef,
clientId: UUID, clientId: UUID,
project: Project project: Project,
): F[ProjectServiceFailure, LanguageServerSockets] = missingComponentAction: MissingComponentAction
languageServerGateway ): F[ProjectServiceFailure, LanguageServerSockets] = for {
.start(clientId, project) 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 { .mapError {
case PreviousInstanceNotShutDown => case PreviousInstanceNotShutDown =>
ProjectOpenFailed( ProjectOpenFailed(
@ -215,6 +260,7 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
s"Language server boot failed: ${th.getMessage}" s"Language server boot failed: ${th.getMessage}"
) )
} }
} yield sockets
/** @inheritdoc */ /** @inheritdoc */
override def closeProject( override def closeProject(

View File

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

View File

@ -28,6 +28,12 @@ object ProjectServiceFailure {
*/ */
case object ProjectNotFound extends 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. /** Signals that a failure occurred during project startup.
* *
* @param message a failure message * @param message a failure message

View File

@ -1,6 +1,9 @@
package org.enso.projectmanager.service.config package org.enso.projectmanager.service.config
import io.circe.Json 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.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure
import org.enso.projectmanager.service.versionmanagement.NoOpInterface import org.enso.projectmanager.service.versionmanagement.NoOpInterface
@ -11,7 +14,7 @@ import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
* *
* @param distributionConfiguration a distribution configuration * @param distributionConfiguration a distribution configuration
*/ */
class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel]( class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
distributionConfiguration: DistributionConfiguration distributionConfiguration: DistributionConfiguration
) extends GlobalConfigServiceApi[F] { ) extends GlobalConfigServiceApi[F] {
@ -20,26 +23,46 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel](
distributionConfiguration.distributionManager distributionConfiguration.distributionManager
) )
/** @inheritdoc */
override def getKey( override def getKey(
key: String key: String
): F[GlobalConfigServiceFailure, Option[String]] = ): F[GlobalConfigServiceFailure, Option[String]] =
Sync[F].blockingIO { Sync[F].blockingOp {
val valueOption = configurationManager.getConfig.original.apply(key) val valueOption = configurationManager.getConfig.original.apply(key)
valueOption.map(json => json.asString.getOrElse(json.toString())) valueOption.map(json => json.asString.getOrElse(json.toString()))
}.recoverAccessErrors }.recoverAccessErrors
/** @inheritdoc */
override def setKey( override def setKey(
key: String, key: String,
value: String value: String
): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingIO { ): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingOp {
configurationManager.updateConfigRaw(key, Json.fromString(value)) configurationManager.updateConfigRaw(key, Json.fromString(value))
}.recoverAccessErrors }.recoverAccessErrors
/** @inheritdoc */
override def deleteKey(key: String): F[GlobalConfigServiceFailure, Unit] = override def deleteKey(key: String): F[GlobalConfigServiceFailure, Unit] =
Sync[F].blockingIO { Sync[F].blockingOp {
configurationManager.removeFromConfig(key) configurationManager.removeFromConfig(key)
}.recoverAccessErrors }.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]) { implicit class AccessErrorRecovery[A](fa: F[Throwable, A]) {
def recoverAccessErrors: F[GlobalConfigServiceFailure, A] = { def recoverAccessErrors: F[GlobalConfigServiceFailure, A] = {
ErrorChannel[F].mapError(fa) { throwable => 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 package org.enso.projectmanager.service.config
import nl.gn0s1s.bump.SemVer
import org.enso.pkg.EnsoVersion
/** A contract for the Global Config Service. /** A contract for the Global Config Service.
* *
* @tparam F a monadic context * @tparam F a monadic context
@ -23,4 +26,19 @@ trait GlobalConfigServiceApi[F[+_, +_]] {
* If the value was not present already, nothing happens. * If the value was not present already, nothing happens.
*/ */
def deleteKey(key: String): F[GlobalConfigServiceFailure, Unit] 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, ComponentRepositoryAccessFailure,
ComponentUninstallationFailure ComponentUninstallationFailure
} }
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
import org.enso.projectmanager.versionmanagement.DistributionConfiguration import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.components.ComponentMissingError import org.enso.runtimeversionmanager.components.ComponentMissingError
@ -19,9 +20,10 @@ import org.enso.runtimeversionmanager.components.ComponentMissingError
* @param distributionConfiguration a distribution configuration * @param distributionConfiguration a distribution configuration
*/ */
class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel]( class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
override val distributionConfiguration: DistributionConfiguration distributionConfiguration: DistributionConfiguration
) extends RuntimeVersionManagementServiceApi[F] ) extends RuntimeVersionManagementServiceApi[F] {
with RuntimeVersionManagerMixin {
val factory = RuntimeVersionManagerFactory(distributionConfiguration)
/** @inheritdoc */ /** @inheritdoc */
override def installEngine( override def installEngine(
@ -31,11 +33,13 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
): F[ProjectServiceFailure, Unit] = { ): F[ProjectServiceFailure, Unit] = {
Sync[F] Sync[F]
.blockingOp { .blockingOp {
makeRuntimeVersionManager( factory
.makeRuntimeVersionManager(
progressTracker, progressTracker,
allowMissingComponents = true, allowMissingComponents = true,
allowBrokenComponents = forceInstallBroken allowBrokenComponents = forceInstallBroken
).findOrInstallEngine(version) )
.findOrInstallEngine(version)
() ()
} }
.mapRuntimeManagerErrors(throwable => .mapRuntimeManagerErrors(throwable =>
@ -50,11 +54,13 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
): F[ProjectServiceFailure, Unit] = Sync[F] ): F[ProjectServiceFailure, Unit] = Sync[F]
.blockingOp { .blockingOp {
try { try {
makeRuntimeVersionManager( factory
.makeRuntimeVersionManager(
progressTracker, progressTracker,
allowMissingComponents = false, allowMissingComponents = false,
allowBrokenComponents = false allowBrokenComponents = false
).uninstallEngine(version) )
.uninstallEngine(version)
} catch { } catch {
case _: ComponentMissingError => case _: ComponentMissingError =>
} }
@ -67,7 +73,7 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
override def listInstalledEngines() override def listInstalledEngines()
: F[ProjectServiceFailure, Seq[EngineVersion]] = Sync[F] : F[ProjectServiceFailure, Seq[EngineVersion]] = Sync[F]
.blockingOp { .blockingOp {
makeReadOnlyVersionManager().listInstalledEngines().map { factory.makeReadOnlyVersionManager().listInstalledEngines().map {
installedEngine => installedEngine =>
EngineVersion(installedEngine.version, installedEngine.isMarkedBroken) 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 EngineRepository
} }
import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider
import org.enso.runtimeversionmanager.runner.JVMSettings
/** Default distribution configuration to use for the Project Manager in /** Default distribution configuration to use for the Project Manager in
* production. * production.
@ -27,13 +28,13 @@ import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider
object DefaultDistributionConfiguration extends DistributionConfiguration { object DefaultDistributionConfiguration extends DistributionConfiguration {
/** The default [[Environment]] implementation, with no overrides. */ /** 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? // TODO [RW, AO] should the PM support portable distributions?
// If so, where will be the project-manager binary located with respect to // If so, where will be the project-manager binary located with respect to
// the distribution root? // the distribution root?
/** @inheritdoc */ /** @inheritdoc */
lazy val distributionManager = new DistributionManager(DefaultEnvironment) lazy val distributionManager = new DistributionManager(environment)
/** @inheritdoc */ /** @inheritdoc */
lazy val lockManager = new FileLockManager(distributionManager.paths.locks) lazy val lockManager = new FileLockManager(distributionManager.paths.locks)
@ -63,4 +64,10 @@ object DefaultDistributionConfiguration extends DistributionConfiguration {
engineReleaseProvider = engineReleaseProvider, engineReleaseProvider = engineReleaseProvider,
runtimeReleaseProvider = runtimeReleaseProvider 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 package org.enso.projectmanager.versionmanagement
import org.enso.runtimeversionmanager.Environment
import org.enso.runtimeversionmanager.components.{ import org.enso.runtimeversionmanager.components.{
RuntimeVersionManagementUserInterface, RuntimeVersionManagementUserInterface,
RuntimeVersionManager RuntimeVersionManager
@ -11,6 +12,7 @@ import org.enso.runtimeversionmanager.distribution.{
import org.enso.runtimeversionmanager.locking.ResourceManager import org.enso.runtimeversionmanager.locking.ResourceManager
import org.enso.runtimeversionmanager.releases.ReleaseProvider import org.enso.runtimeversionmanager.releases.ReleaseProvider
import org.enso.runtimeversionmanager.releases.engine.EngineRelease import org.enso.runtimeversionmanager.releases.engine.EngineRelease
import org.enso.runtimeversionmanager.runner.JVMSettings
/** Specifies the configuration of project manager's distribution. /** Specifies the configuration of project manager's distribution.
* *
@ -20,6 +22,9 @@ import org.enso.runtimeversionmanager.releases.engine.EngineRelease
*/ */
trait DistributionConfiguration { trait DistributionConfiguration {
/** An [[Environment]] instance. */
def environment: Environment
/** A [[DistributionManager]] instance. */ /** A [[DistributionManager]] instance. */
def distributionManager: DistributionManager def distributionManager: DistributionManager
@ -39,4 +44,19 @@ trait DistributionConfiguration {
def makeRuntimeVersionManager( def makeRuntimeVersionManager(
userInterface: RuntimeVersionManagementUserInterface userInterface: RuntimeVersionManagementUserInterface
): RuntimeVersionManager ): 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 io-timeout = 5 seconds
request-timeout = 10 seconds request-timeout = 10 seconds
boot-timeout = 30 seconds boot-timeout = 30 seconds
shutdown-timeout = 10 seconds shutdown-timeout = 20 seconds
socket-close-timeout = 2 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 package org.enso.projectmanager
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.{Files, Path}
import java.time.{OffsetDateTime, ZoneOffset} import java.time.{OffsetDateTime, ZoneOffset}
import java.util.UUID import java.util.UUID
import akka.testkit.TestActors.blackholeProps
import akka.testkit._ 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.apache.commons.io.FileUtils
import org.enso.jsonrpc.test.JsonRpcServerTestKit import org.enso.jsonrpc.test.JsonRpcServerTestKit
import org.enso.jsonrpc.{ClientControllerFactory, Protocol} 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.Globals.{ConfigFilename, ConfigNamespace}
import org.enso.projectmanager.boot.configuration._ import org.enso.projectmanager.boot.configuration._
import org.enso.projectmanager.control.effect.ZioEnvExec import org.enso.projectmanager.control.effect.ZioEnvExec
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
import org.enso.projectmanager.infrastructure.languageserver.{ import org.enso.projectmanager.infrastructure.languageserver.{
ExecutorWithUnlimitedPool,
LanguageServerGatewayImpl, LanguageServerGatewayImpl,
LanguageServerRegistry, LanguageServerRegistry,
ShutdownHookActivator ShutdownHookActivator
@ -26,18 +33,27 @@ import org.enso.projectmanager.protocol.{
} }
import org.enso.projectmanager.service.config.GlobalConfigService import org.enso.projectmanager.service.config.GlobalConfigService
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService 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.projectmanager.test.{ObservableGenerator, ProgrammableClock}
import org.enso.runtimeversionmanager.OS
import org.enso.runtimeversionmanager.test.{DropLogs, FakeReleases} import org.enso.runtimeversionmanager.test.{DropLogs, FakeReleases}
import org.scalatest.BeforeAndAfterAll
import pureconfig.ConfigSource import pureconfig.ConfigSource
import pureconfig.generic.auto._ import pureconfig.generic.auto._
import zio.interop.catz.core._ import zio.interop.catz.core._
import zio.{Runtime, Semaphore, ZEnv, ZIO} import zio.{Runtime, Semaphore, ZEnv, ZIO}
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._ 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 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 projectValidator = new MonadicProjectValidator[ZIO[ZEnv, *, *]]()
lazy val distributionConfiguration =
TestDistributionConfiguration(
distributionRoot = testDistributionRoot.toPath,
engineReleaseProvider = FakeReleases.engineReleaseProvider,
runtimeReleaseProvider = FakeReleases.runtimeReleaseProvider,
discardChildOutput = !debugChildLogs
)
lazy val languageServerRegistry = lazy val languageServerRegistry =
system.actorOf( system.actorOf(
LanguageServerRegistry LanguageServerRegistry
.props(netConfig, bootloaderConfig, supervisionConfig, timeoutConfig) .props(
netConfig,
bootloaderConfig,
supervisionConfig,
timeoutConfig,
distributionConfiguration,
ExecutorWithUnlimitedPool
)
) )
lazy val shutdownHookActivator = lazy val shutdownHookActivator =
@ -114,24 +145,23 @@ class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
timeoutConfig timeoutConfig
) )
lazy val projectCreationService =
new ProjectCreationService[ZIO[ZEnv, +*, +*]](distributionConfiguration)
lazy val globalConfigService = new GlobalConfigService[ZIO[ZEnv, +*, +*]](
distributionConfiguration
)
lazy val projectService = lazy val projectService =
new ProjectService[ZIO[ZEnv, +*, +*]]( new ProjectService[ZIO[ZEnv, +*, +*]](
projectValidator, projectValidator,
projectRepository, projectRepository,
projectCreationService,
globalConfigService,
new Slf4jLogging[ZIO[ZEnv, +*, +*]], new Slf4jLogging[ZIO[ZEnv, +*, +*]],
testClock, testClock,
gen, gen,
languageServerGateway languageServerGateway,
)
lazy val distributionConfiguration =
TestDistributionConfiguration(
distributionRoot = testDistributionRoot.toPath,
engineReleaseProvider = FakeReleases.engineReleaseProvider,
runtimeReleaseProvider = FakeReleases.runtimeReleaseProvider
)
lazy val globalConfigService = new GlobalConfigService[ZIO[ZEnv, +*, +*]](
distributionConfiguration 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 = { override def afterEach(): Unit = {
super.afterEach() super.afterEach()
if (deleteProjectsRootAfterEachTest)
FileUtils.deleteQuietly(testProjectsRoot) 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 java.util.UUID
import akka.testkit.TestDuration
import io.circe.Json import io.circe.Json
import io.circe.syntax._ import io.circe.syntax._
import io.circe.literal._ 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 { val socket = for {
result <- openReply.hcursor.downExpectedField("result") result <- openReply.hcursor.downExpectedField("result")
addr <- result.downExpectedField("languageServerJsonAddress") addr <- result.downExpectedField("languageServerJsonAddress")
@ -81,13 +82,16 @@ trait ProjectManagementOps { this: BaseServerSpec =>
} }
} }
""") """)
client.expectJson(json""" client.expectJson(
json"""
{ {
"jsonrpc":"2.0", "jsonrpc":"2.0",
"id":0, "id":0,
"result": null "result": null
} }
""") """,
10.seconds.dilated
)
} }
def deleteProject( def deleteProject(

View File

@ -25,12 +25,14 @@ import org.enso.runtimeversionmanager.releases.{
ReleaseProvider, ReleaseProvider,
SimpleReleaseProvider SimpleReleaseProvider
} }
import org.enso.runtimeversionmanager.runner.{JVMSettings, JavaCommand}
import org.enso.runtimeversionmanager.test.{ import org.enso.runtimeversionmanager.test.{
FakeEnvironment, FakeEnvironment,
HasTestDirectory, HasTestDirectory,
TestLocalLockManager TestLocalLockManager
} }
import scala.jdk.OptionConverters.RichOptional
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
/** A distribution configuration for use in tests. /** A distribution configuration for use in tests.
@ -39,20 +41,23 @@ import scala.util.{Failure, Success, Try}
* within some temporary directory * within some temporary directory
* @param engineReleaseProvider provider of (fake) engine releases * @param engineReleaseProvider provider of (fake) engine releases
* @param runtimeReleaseProvider provider of (fake) Graal releases * @param runtimeReleaseProvider provider of (fake) Graal releases
* @param discardChildOutput specifies if input of launched runner processes
* should be ignored
*/ */
class TestDistributionConfiguration( class TestDistributionConfiguration(
distributionRoot: Path, distributionRoot: Path,
override val engineReleaseProvider: ReleaseProvider[EngineRelease], override val engineReleaseProvider: ReleaseProvider[EngineRelease],
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider runtimeReleaseProvider: GraalVMRuntimeReleaseProvider,
discardChildOutput: Boolean
) extends DistributionConfiguration ) extends DistributionConfiguration
with FakeEnvironment with FakeEnvironment
with HasTestDirectory { with HasTestDirectory {
def getTestDirectory: Path = distributionRoot def getTestDirectory: Path = distributionRoot
lazy val distributionManager = new DistributionManager( lazy val environment = fakeInstalledEnvironment()
fakeInstalledEnvironment()
) lazy val distributionManager = new DistributionManager(environment)
lazy val lockManager = new TestLocalLockManager lazy val lockManager = new TestLocalLockManager
@ -71,11 +76,60 @@ class TestDistributionConfiguration(
engineReleaseProvider = engineReleaseProvider, engineReleaseProvider = engineReleaseProvider,
runtimeReleaseProvider = runtimeReleaseProvider runtimeReleaseProvider = runtimeReleaseProvider
) )
/** 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 { object TestDistributionConfiguration {
def withoutReleases(distributionRoot: Path): TestDistributionConfiguration = {
val noReleaseProvider = new SimpleReleaseProvider { /** 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,
discardChildOutput: Boolean
): TestDistributionConfiguration =
new TestDistributionConfiguration(
distributionRoot,
engineReleaseProvider,
runtimeReleaseProvider,
discardChildOutput
)
/** A [[SimpleReleaseProvider]] that has no releases. */
private class NoReleaseProvider extends SimpleReleaseProvider {
override def releaseForTag(tag: String): Try[Release] = Failure( override def releaseForTag(tag: String): Try[Release] = Failure(
new IllegalStateException( new IllegalStateException(
"This provider does not support fetching releases." "This provider does not support fetching releases."
@ -84,23 +138,4 @@ object TestDistributionConfiguration {
override def listReleases(): Try[Seq[Release]] = Success(Seq()) override def listReleases(): Try[Seq[Release]] = Success(Seq())
} }
new TestDistributionConfiguration(
distributionRoot = distributionRoot,
engineReleaseProvider = new EngineReleaseProvider(noReleaseProvider),
runtimeReleaseProvider = new GraalCEReleaseProvider(noReleaseProvider)
)
}
def apply(
distributionRoot: Path,
engineReleaseProvider: ReleaseProvider[EngineRelease],
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider
): TestDistributionConfiguration =
new TestDistributionConfiguration(
distributionRoot,
engineReleaseProvider,
runtimeReleaseProvider
)
} }

View File

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

View File

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

View File

@ -64,7 +64,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
} }
} }
""") """)
client.expectJson( client.expectTaskStarted()
client.expectJsonAfterSomeProgress(
json""" json"""
{ {
"jsonrpc":"2.0", "jsonrpc":"2.0",
@ -84,7 +85,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
} }
} }
""") """)
client.expectJson( client.expectTaskStarted()
client.expectJsonAfterSomeProgress(
json""" json"""
{ {
"jsonrpc":"2.0", "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""" client.expectJson(json"""
{ {
"jsonrpc":"2.0", "jsonrpc":"2.0",
"id":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""" json"""
{ {
"jsonrpc":"2.0", "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 java.util.UUID
import io.circe.literal._ import io.circe.literal._
import nl.gn0s1s.bump.SemVer
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.enso.projectmanager.test.Net.tryConnect import org.enso.projectmanager.test.Net.tryConnect
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps} import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
@ -22,6 +23,8 @@ class ProjectManagementApiSpec
gen.reset() gen.reset()
} }
override val engineToInstall = Some(SemVer(0, 0, 1))
"project/create" must { "project/create" must {
"check if project name is not empty" taggedAs Flaky in { "check if project name is not empty" taggedAs Flaky in {
@ -31,7 +34,8 @@ class ProjectManagementApiSpec
"method": "project/create", "method": "project/create",
"id": 1, "id": 1,
"params": { "params": {
"name": "" "name": "",
"missingComponentAction": "Install"
} }
} }
""") """)
@ -124,26 +128,23 @@ class ProjectManagementApiSpec
} }
"create project with specific version" in { "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) implicit val client = new WsTestClient(address)
client.send(json""" client.send(json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"method": "project/create", "method": "project/create",
"id": 0, "id": 1,
"params": { "params": {
"name": "foo", "name": "foo",
"version": "1.2.3" "version": "0.0.1"
} }
} }
""") """)
client.expectJson(json""" client.expectJson(json"""
{ {
"jsonrpc":"2.0", "jsonrpc" : "2.0",
"id":0, "id" : 1,
"error":{ "result" : {
"code":10, "projectId" : $getGeneratedUUID
"message":"The requested method is not implemented"
} }
} }
""") """)

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 minimum-launcher-version: 0.0.1
graal-vm-version: 2.0.0 graal-vm-version: 2.0.0
graal-java-version: 11 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 com.typesafe.scalalogging.Logger
import nl.gn0s1s.bump.SemVer 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 * In development-mode it allows to override the returned version for testing
* purposes. * purposes.

View File

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

View File

@ -20,18 +20,7 @@ class ResourceManager(lockManager: LockManager) {
resource: Resource, resource: Resource,
lockType: LockType lockType: LockType
)(action: => R): R = { )(action: => R): R = {
var waited = false Using(acquireResource(waitingInterface, resource, lockType)) { _ =>
Using {
lockManager.acquireLockWithWaitingAction(
resource.name,
lockType = lockType,
() => {
waited = true
waitingInterface.startWaitingForResource(resource)
}
)
} { _ =>
if (waited) waitingInterface.finishWaitingForResource(resource)
action action
}.get }.get
} }
@ -52,6 +41,27 @@ class ResourceManager(lockManager: LockManager) {
action 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 var mainLock: Option[Lock] = None
/** Initializes the [[MainLock]]. /** Initializes the [[MainLock]].

View File

@ -21,10 +21,7 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
def run(): Try[Int] = def run(): Try[Int] =
wrapError { wrapError {
logger.debug(s"Executing $toString") logger.debug(s"Executing $toString")
val processBuilder = new java.lang.ProcessBuilder(command: _*) val processBuilder = builder()
for ((key, value) <- extraEnv) {
processBuilder.environment().put(key, value)
}
processBuilder.inheritIO() processBuilder.inheritIO()
val process = processBuilder.start() val process = processBuilder.start()
process.waitFor() process.waitFor()
@ -44,6 +41,21 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
processBuilder.!! 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]] /** Runs the provided action and wraps any errors into a [[Failure]]
* containing a [[RunnerError]]. * containing a [[RunnerError]].
*/ */

View File

@ -1,9 +1,38 @@
package org.enso.runtimeversionmanager.runner package org.enso.runtimeversionmanager.runner
/** Represents settings that are used to launch the runtime JVM. /** Represents settings that are used to launch the runtime JVM.
*
* @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(
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 * @param useSystemJVM if set, the system configured JVM is used instead of
* the one managed by the launcher * the one managed by the launcher
* @param jvmOptions options that should be added to the launched JVM * @param jvmOptions options that should be added to the launched JVM
*/ */
case class JVMSettings(useSystemJVM: Boolean, jvmOptions: Seq[(String, String)]) 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. /** 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 runnerArguments arguments that should be passed to the runner
* @param connectLoggerIfAvailable specifies if the ran component should * @param connectLoggerIfAvailable specifies if the ran component should
* connect to launcher's logging service * connect to launcher's logging service
*/ */
case class RunSettings( case class RunSettings(
version: SemVer, engineVersion: SemVer,
runnerArguments: Seq[String], runnerArguments: Seq[String],
connectLoggerIfAvailable: Boolean connectLoggerIfAvailable: Boolean
) )

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