mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 16:01:30 +03:00
Forking Language Server in the Project Manager (#1305)
This commit is contained in:
parent
a40989e7c6
commit
9e1b49d245
9
.github/workflows/scala.yml
vendored
9
.github/workflows/scala.yml
vendored
@ -133,6 +133,11 @@ jobs:
|
||||
# test run. It is built before starting the tests to conserve system
|
||||
# memory
|
||||
echo "LAUNCHER_NATIVE_IMAGE_TEST_SKIP_BUILD=true" >> $GITHUB_ENV
|
||||
- name: Build the Runner & Runtime Uberjars
|
||||
run: |
|
||||
sleep 1
|
||||
sbt --no-colors engine-runner/assembly
|
||||
|
||||
- name: Test Enso
|
||||
run: |
|
||||
sleep 1
|
||||
@ -151,10 +156,6 @@ jobs:
|
||||
sbt --no-colors searcher/Benchmark/compile
|
||||
|
||||
# Build Distribution
|
||||
- name: Build the Runner & Runtime Uberjars
|
||||
run: |
|
||||
sleep 1
|
||||
sbt --no-colors engine-runner/assembly
|
||||
- name: Build the Project Manager Uberjar
|
||||
run: |
|
||||
sleep 1
|
||||
|
42
build.sbt
42
build.sbt
@ -609,6 +609,26 @@ lazy val `logging-service` = project
|
||||
)
|
||||
.dependsOn(`akka-native`)
|
||||
|
||||
ThisBuild / testOptions += Tests.Setup(_ =>
|
||||
// Note [Logging Service in Tests]
|
||||
sys.props("org.enso.loggingservice.test-log-level") = "2"
|
||||
)
|
||||
|
||||
/* Note [Logging Service in Tests]
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* As migrating the runner to our new logging service has forced us to migrate
|
||||
* other components that are related to it too, some tests that used to be
|
||||
* configured by logback are not properly configured anymore and log a lot of
|
||||
* debug information. This is a temporary fix to make sure that there are not
|
||||
* too much logs in the CI - it sets the log level for all tests to be at most
|
||||
* info by default. It can be overridden by particular tests if they set up a
|
||||
* logging service.
|
||||
*
|
||||
* This is a temporary solution and it will be obsoleted once all of our
|
||||
* components do a complete migration to the new logging service, which is
|
||||
* planned in tasks #1144 and #1151.
|
||||
*/
|
||||
|
||||
lazy val cli = project
|
||||
.in(file("lib/scala/cli"))
|
||||
.configs(Test)
|
||||
@ -647,15 +667,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
|
||||
(Compile / run / fork) := true,
|
||||
(Test / fork) := true,
|
||||
(Compile / run / connectInput) := true,
|
||||
javaOptions ++= {
|
||||
// Note [Classpath Separation]
|
||||
val runtimeClasspath =
|
||||
(runtime / Compile / fullClasspath).value
|
||||
.map(_.data)
|
||||
.mkString(File.pathSeparator)
|
||||
Seq(s"-Dtruffle.class.path.append=$runtimeClasspath")
|
||||
},
|
||||
libraryDependencies ++= akka,
|
||||
libraryDependencies ++= akka ++ Seq(akkaTestkit % Test),
|
||||
libraryDependencies ++= circe,
|
||||
libraryDependencies ++= Seq(
|
||||
"com.typesafe" % "config" % typesafeConfigVersion,
|
||||
@ -665,6 +677,7 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
|
||||
"dev.zio" %% "zio-interop-cats" % zioInteropCatsVersion,
|
||||
"commons-cli" % "commons-cli" % commonsCliVersion,
|
||||
"commons-io" % "commons-io" % commonsIoVersion,
|
||||
"org.apache.commons" % "commons-lang3" % commonsLangVersion,
|
||||
"com.beachape" %% "enumeratum-circe" % enumeratumCirceVersion,
|
||||
"com.miguno.akka" %% "akka-mock-scheduler" % akkaMockSchedulerVersion % Test,
|
||||
"org.mockito" %% "mockito-scala" % mockitoScalaVersion % Test
|
||||
@ -697,13 +710,12 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
|
||||
)
|
||||
)
|
||||
),
|
||||
assembly := assembly
|
||||
.dependsOn(runtime / assembly)
|
||||
.value
|
||||
assembly := assembly.dependsOn(`engine-runner` / assembly).value,
|
||||
(Test / test) := (Test / test).dependsOn(`engine-runner` / assembly).value
|
||||
)
|
||||
.dependsOn(`version-output`)
|
||||
.dependsOn(pkg)
|
||||
.dependsOn(`language-server`)
|
||||
.dependsOn(`polyglot-api`)
|
||||
.dependsOn(`runtime-version-manager`)
|
||||
.dependsOn(`json-rpc-server`)
|
||||
.dependsOn(`json-rpc-server-test` % Test)
|
||||
@ -867,8 +879,7 @@ lazy val `polyglot-api` = project
|
||||
|
||||
lazy val `language-server` = (project in file("engine/language-server"))
|
||||
.settings(
|
||||
libraryDependencies ++= akka ++ akkaTest ++ circe ++ Seq(
|
||||
"ch.qos.logback" % "logback-classic" % logbackClassicVersion,
|
||||
libraryDependencies ++= akka ++ circe ++ Seq(
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
|
||||
"io.circe" %% "circe-generic-extras" % circeGenericExtrasVersion,
|
||||
"io.circe" %% "circe-literal" % circeVersion,
|
||||
@ -902,6 +913,7 @@ lazy val `language-server` = (project in file("engine/language-server"))
|
||||
.dependsOn(`text-buffer`)
|
||||
.dependsOn(`searcher`)
|
||||
.dependsOn(testkit % Test)
|
||||
.dependsOn(`logging-service`)
|
||||
|
||||
lazy val ast = (project in file("lib/scala/ast"))
|
||||
.settings(
|
||||
|
@ -261,11 +261,6 @@ The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `com.google.auto.service.auto-service-annotations-1.0-rc7`.
|
||||
|
||||
|
||||
'logback-classic', licensed under the GNU Lesser General Public License, is distributed with the engine.
|
||||
The license information can be found along with the copyright notices.
|
||||
Copyright notices related to this dependency can be found in the directory `ch.qos.logback.logback-classic-1.2.3`.
|
||||
|
||||
|
||||
'scala-collection-compat_2.13', licensed under the Apache-2.0, is distributed with the engine.
|
||||
The license file can be found at `licenses/APACHE2.0`.
|
||||
Copyright notices related to this dependency can be found in the directory `org.scala-lang.modules.scala-collection-compat_2.13-2.0.0`.
|
||||
@ -466,11 +461,6 @@ The license information can be found along with the copyright notices.
|
||||
Copyright notices related to this dependency can be found in the directory `com.beachape.enumeratum_2.13-1.6.1`.
|
||||
|
||||
|
||||
'logback-core', licensed under the GNU Lesser General Public License, is distributed with the engine.
|
||||
The license information can be found along with the copyright notices.
|
||||
Copyright notices related to this dependency can be found in the directory `ch.qos.logback.logback-core-1.2.3`.
|
||||
|
||||
|
||||
'pureconfig_2.13', licensed under the Mozilla Public License, version 2.0, is distributed with the engine.
|
||||
The license information can be found along with the copyright notices.
|
||||
Copyright notices related to this dependency can be found in the directory `com.github.pureconfig.pureconfig_2.13-0.13.0`.
|
||||
|
@ -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>
|
@ -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.
|
@ -1 +0,0 @@
|
||||
Please see ch.qos.logback.logback-classic-1.2 for notices related to logback.
|
@ -116,6 +116,12 @@ The review can be performed manually by modifying the settings inside of the
|
||||
|
||||
#### Review Process
|
||||
|
||||
> The updates performed using the web script are remembered locally, so they
|
||||
> **will not show up after the refresh**. If you ever need to open the edit mode
|
||||
> after closing its window, you should re-generate the report using
|
||||
> `enso/gatherLicenses` or just open it using `enso/openLegalReviewReport` which
|
||||
> will refresh it automatically.
|
||||
|
||||
1. Open the review in edit mode using the helper script.
|
||||
- You can type `enso / openLegalReviewReport` if you have `npm` in your PATH
|
||||
as visible from SBT.
|
||||
@ -192,12 +198,6 @@ The review can be performed manually by modifying the settings inside of the
|
||||
- Ensure that there are no more warnings, and if there are any go back to fix
|
||||
the issues.
|
||||
|
||||
The updates performed using the web script are remembered locally, so they will
|
||||
not show up after the refresh. If you ever need to open the edit mode after
|
||||
closing its window, you should re-generate the report using
|
||||
`enso/gatherLicenses` or just open it using `enso/openLegalReviewReport` which
|
||||
will refresh it automatically.
|
||||
|
||||
#### Additional Manual Considerations
|
||||
|
||||
The Scala Library notice contains the following mention:
|
||||
|
@ -65,6 +65,8 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`executionContext/canModify`](#executioncontextcanmodify)
|
||||
- [`executionContext/receivesUpdates`](#executioncontextreceivesupdates)
|
||||
- [`search/receivesSuggestionsDatabaseUpdates`](#searchreceivessuggestionsdatabaseupdates)
|
||||
- [Enables](#enables-4)
|
||||
- [Disables](#disables-4)
|
||||
- [File Management Operations](#file-management-operations)
|
||||
- [`file/write`](#filewrite)
|
||||
- [`file/read`](#fileread)
|
||||
@ -94,6 +96,9 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`workspace/redo`](#workspaceredo)
|
||||
- [Monitoring](#monitoring)
|
||||
- [`heartbeat/ping`](#heartbeatping)
|
||||
- [`heartbeat/init`](#heartbeatinit)
|
||||
- [Refactoring](#refactoring)
|
||||
- [`refactoring/renameProject`](#refactoringrenameproject)
|
||||
- [Execution Management Operations](#execution-management-operations)
|
||||
- [Execution Management Example](#execution-management-example)
|
||||
- [Create Execution Context](#create-execution-context)
|
||||
@ -113,9 +118,9 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`executionContext/modifyVisualisation`](#executioncontextmodifyvisualisation)
|
||||
- [`executionContext/visualisationUpdate`](#executioncontextvisualisationupdate)
|
||||
- [Search Operations](#search-operations)
|
||||
- [Suggestions Database Example](#suggestionsdatabaseexample)
|
||||
- [Suggestions Database Example](#suggestions-database-example)
|
||||
- [`search/getSuggestionsDatabase`](#searchgetsuggestionsdatabase)
|
||||
- [`search/invalidateSuggestionsDatabase`](#invalidatesuggestionsdatabase)
|
||||
- [`search/invalidateSuggestionsDatabase`](#searchinvalidatesuggestionsdatabase)
|
||||
- [`search/getSuggestionsDatabaseVersion`](#searchgetsuggestionsdatabaseversion)
|
||||
- [`search/suggestionsDatabaseUpdate`](#searchsuggestionsdatabaseupdate)
|
||||
- [`search/completion`](#searchcompletion)
|
||||
@ -124,17 +129,17 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`io/redirectStandardOutput`](#ioredirectstdardoutput)
|
||||
- [`io/suppressStandardOutput`](#iosuppressstdardoutput)
|
||||
- [`io/standardOutputAppended`](#iostandardoutputappended)
|
||||
- [`io/redirectStandardError`](#ioredirectstdarderror)
|
||||
- [`io/suppressStandardError`](#iosuppressstdarderror)
|
||||
- [`io/redirectStandardError`](#ioredirectstandarderror)
|
||||
- [`io/suppressStandardError`](#iosuppressstandarderror)
|
||||
- [`io/standardErrorAppended`](#iostandarderrorappended)
|
||||
- [`io/feedStandardInput`](#iofeedstandardinput)
|
||||
- [`io/waitingForStandardInput`](#iowaitingforstandardinput)
|
||||
- [Errors](#errors)
|
||||
- [Errors](#errors-57)
|
||||
- [`AccessDeniedError`](#accessdeniederror)
|
||||
- [`FileSystemError`](#filesystemerror)
|
||||
- [`ContentRootNotFoundError`](#contentrootnotfounderror)
|
||||
- [`FileNotFound`](#filenotfound)
|
||||
- [`FileExists`](#fileexists-1)
|
||||
- [`FileExists`](#fileexists)
|
||||
- [`OperationTimeoutError`](#operationtimeouterror)
|
||||
- [`NotDirectory`](#notdirectory)
|
||||
- [`StackItemNotFoundError`](#stackitemnotfounderror)
|
||||
@ -145,7 +150,6 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`VisualisationNotFoundError`](#visualisationnotfounderror)
|
||||
- [`VisualisationExpressionError`](#visualisationexpressionerror)
|
||||
- [`VisualisationEvaluationError`](#visualisationevaluationerror)
|
||||
- [`ExecutionFailedError`](#executionfailederror)
|
||||
- [`FileNotOpenedError`](#filenotopenederror)
|
||||
- [`TextEditValidationError`](#texteditvalidationerror)
|
||||
- [`InvalidVersionError`](#invalidversionerror)
|
||||
@ -154,6 +158,8 @@ transport formats, please look [here](./protocol-architecture).
|
||||
- [`SessionNotInitialisedError`](#sessionnotinitialisederror)
|
||||
- [`SessionAlreadyInitialisedError`](#sessionalreadyinitialisederror)
|
||||
- [`SuggestionsDatabaseError`](#suggestionsdatabaseerror)
|
||||
- [`ProjectNotFoundError`](#projectnotfounderror)
|
||||
- [`ModuleNameNotResolvedError`](#modulenamenotresolvederror)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
@ -2085,6 +2091,33 @@ null;
|
||||
|
||||
None
|
||||
|
||||
### `heartbeat/init`
|
||||
|
||||
This request is sent from the bootloader to check if the started language server
|
||||
instance has finished initialization. A reply should only be sent when the main
|
||||
module has been fully initialized.
|
||||
|
||||
- **Type:** Request
|
||||
- **Direction:** Supervisor -> Server
|
||||
- **Connection:** Protocol
|
||||
- **Visibility:** Private
|
||||
|
||||
#### Parameters
|
||||
|
||||
```typescript
|
||||
null;
|
||||
```
|
||||
|
||||
#### Result
|
||||
|
||||
```typescript
|
||||
null;
|
||||
```
|
||||
|
||||
#### Errors
|
||||
|
||||
None
|
||||
|
||||
## Refactoring
|
||||
|
||||
The language server also provides refactoring operations to restructure an
|
||||
|
@ -65,6 +65,7 @@ transport formats, please look [here](./protocol-architecture.md).
|
||||
- [`ProjectCloseError`](#projectcloseerror)
|
||||
- [`LanguageServerError`](#languageservererror)
|
||||
- [`GlobalConfigurationAccessError`](#globalconfigurationaccesserror)
|
||||
- [`ProjectCreateError`](#projectcreateerror)
|
||||
- [`LoggingServiceUnavailable`](#loggingserviceunavailable)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
@ -143,8 +144,8 @@ operation also includes spawning an instance of the language server open on the
|
||||
specified project.
|
||||
|
||||
To open a project, an engine version that is specified in project settings needs
|
||||
to be installed. If `missingComponentAction` is set to `install` or
|
||||
`force-install-broken`, this action will install any missing components,
|
||||
to be installed. If `missingComponentAction` is set to `Install` or
|
||||
`ForceInstallBroken`, this action will install any missing components,
|
||||
otherwise, an error will be reported if a component is missing. A typical usage
|
||||
scenario may consist of first trying to open the project without installing
|
||||
missing components. If that fails with the `MissingComponentError`, the client
|
||||
@ -165,7 +166,7 @@ interface ProjectOpenRequest {
|
||||
/**
|
||||
* Specifies how to handle missing components.
|
||||
*
|
||||
* If not provided, defaults to `fail`.
|
||||
* If not provided, defaults to `Fail`.
|
||||
*/
|
||||
missingComponentAction?: MissingComponentAction;
|
||||
}
|
||||
@ -303,7 +304,7 @@ interface ProjectCreateRequest {
|
||||
/**
|
||||
* Specifies how to handle missing components.
|
||||
*
|
||||
* If not provided, defaults to `fail`.
|
||||
* If not provided, defaults to `Fail`.
|
||||
*/
|
||||
missingComponentAction?: MissingComponentAction;
|
||||
}
|
||||
@ -1045,13 +1046,24 @@ Signals that the global configuration file could not be accessed or parsed.
|
||||
}
|
||||
```
|
||||
|
||||
### `ProjectCreateError`
|
||||
|
||||
Signals that an error occurred when creating the project.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 4012,
|
||||
"message" : "Could not create the project."
|
||||
}
|
||||
```
|
||||
|
||||
### `LoggingServiceUnavailable`
|
||||
|
||||
Signals that the logging service is not available.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 4012,
|
||||
"code" : 4013,
|
||||
"message" : "The logging service has failed to boot."
|
||||
}
|
||||
```
|
||||
|
@ -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>
|
@ -14,6 +14,7 @@ import org.enso.languageserver.runtime.RuntimeKiller.{
|
||||
RuntimeShutdownResult,
|
||||
ShutDownRuntime
|
||||
}
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
@ -21,8 +22,9 @@ import scala.concurrent.{Await, Future}
|
||||
/** A lifecycle component used to start and stop a Language Server.
|
||||
*
|
||||
* @param config a LS config
|
||||
* @param logLevel log level for the Language Server
|
||||
*/
|
||||
class LanguageServerComponent(config: LanguageServerConfig)
|
||||
class LanguageServerComponent(config: LanguageServerConfig, logLevel: LogLevel)
|
||||
extends LifecycleComponent
|
||||
with LazyLogging {
|
||||
|
||||
@ -34,7 +36,7 @@ class LanguageServerComponent(config: LanguageServerConfig)
|
||||
/** @inheritdoc */
|
||||
override def start(): Future[ComponentStarted.type] = {
|
||||
logger.info("Starting Language Server...")
|
||||
val module = new MainModule(config)
|
||||
val module = new MainModule(config, logLevel)
|
||||
val initMainModule =
|
||||
for {
|
||||
_ <- module.init
|
||||
|
@ -2,7 +2,6 @@ package org.enso.languageserver.boot
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.logging.ConsoleHandler
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import org.enso.jsonrpc.JsonRpcServer
|
||||
@ -29,8 +28,8 @@ import org.enso.languageserver.runtime._
|
||||
import org.enso.languageserver.search.SuggestionsHandler
|
||||
import org.enso.languageserver.session.SessionRouter
|
||||
import org.enso.languageserver.text.BufferRegistry
|
||||
import org.enso.languageserver.util.Logging
|
||||
import org.enso.languageserver.util.binary.BinaryEncoder
|
||||
import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel}
|
||||
import org.enso.polyglot.{LanguageInfo, RuntimeOptions, RuntimeServerInfo}
|
||||
import org.enso.searcher.sql.{SqlDatabase, SqlSuggestionsRepo, SqlVersionsRepo}
|
||||
import org.enso.text.{ContentBasedVersioning, Sha3_224VersionCalculator}
|
||||
@ -45,8 +44,9 @@ import scala.util.{Failure, Success}
|
||||
/** A main module containing all components of the server.
|
||||
*
|
||||
* @param serverConfig configuration for the language server
|
||||
* @param logLevel log level for the Language Server
|
||||
*/
|
||||
class MainModule(serverConfig: LanguageServerConfig) {
|
||||
class MainModule(serverConfig: LanguageServerConfig, logLevel: LogLevel) {
|
||||
|
||||
val log = LoggerFactory.getLogger(this.getClass)
|
||||
log.trace("Initializing...")
|
||||
@ -150,21 +150,16 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
val stdIn = new ObservablePipedInputStream(stdInSink)
|
||||
|
||||
log.trace("Initializing Runtime context...")
|
||||
val logHandler = Logging.getLogHandler(LanguageInfo.ID) match {
|
||||
case Left(t) =>
|
||||
log.warn("Failed to create the Runtime logger", t)
|
||||
new ConsoleHandler()
|
||||
case Right(handler) =>
|
||||
log.trace(s"Setting Runtime logger")
|
||||
handler
|
||||
}
|
||||
val context = Context
|
||||
.newBuilder(LanguageInfo.ID)
|
||||
.allowAllAccess(true)
|
||||
.allowExperimentalOptions(true)
|
||||
.option(RuntimeServerInfo.ENABLE_OPTION, "true")
|
||||
.option(RuntimeOptions.PACKAGES_PATH, serverConfig.contentRootPath)
|
||||
.option(RuntimeOptions.LOG_LEVEL, logHandler.getLevel.toString)
|
||||
.option(
|
||||
RuntimeOptions.LOG_LEVEL,
|
||||
JavaLoggingLogHandler.getJavaLogLevelFor(logLevel).getName
|
||||
)
|
||||
.option(
|
||||
RuntimeServerInfo.JOB_PARALLELISM_OPTION,
|
||||
Runtime.getRuntime.availableProcessors().toString
|
||||
@ -172,7 +167,9 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
.out(stdOut)
|
||||
.err(stdErr)
|
||||
.in(stdIn)
|
||||
.logHandler(logHandler)
|
||||
.logHandler(
|
||||
JavaLoggingLogHandler.create(JavaLoggingLogHandler.defaultLevelMapping)
|
||||
)
|
||||
.serverTransport((uri: URI, peerEndpoint: MessageEndpoint) => {
|
||||
if (uri.toString == RuntimeServerInfo.URI) {
|
||||
val connection = new RuntimeConnector.Endpoint(
|
||||
@ -187,8 +184,7 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
context.initialize(LanguageInfo.ID)
|
||||
log.trace("Runtime context initialized")
|
||||
|
||||
val logLevel = Logging.LogLevel.fromJava(logHandler.getLevel)
|
||||
system.eventStream.setLogLevel(Logging.LogLevel.toAkka(logLevel))
|
||||
system.eventStream.setLogLevel(LogLevel.toAkka(logLevel))
|
||||
log.trace(s"Set akka log level to $logLevel")
|
||||
|
||||
val runtimeKiller =
|
||||
@ -267,9 +263,18 @@ class MainModule(serverConfig: LanguageServerConfig) {
|
||||
log.error("Failed to initialize SQL versions repo", ex)
|
||||
}(system.dispatcher)
|
||||
|
||||
Future
|
||||
val initialization = Future
|
||||
.sequence(Seq(suggestionsRepoInit, versionsRepoInit))
|
||||
.map(_ => ())
|
||||
|
||||
initialization.onComplete {
|
||||
case Success(()) =>
|
||||
system.eventStream.publish(InitializedEvent.InitializationFinished)
|
||||
case _ =>
|
||||
system.eventStream.publish(InitializedEvent.InitializationFailed)
|
||||
}
|
||||
|
||||
initialization
|
||||
}
|
||||
|
||||
/** Close the main module releasing all resources. */
|
||||
|
@ -7,4 +7,6 @@ object InitializedEvent {
|
||||
|
||||
case object SuggestionsRepoInitialized extends InitializedEvent
|
||||
case object FileVersionsRepoInitialized extends InitializedEvent
|
||||
case object InitializationFinished extends InitializedEvent
|
||||
case object InitializationFailed extends InitializedEvent
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,12 +21,15 @@ import org.enso.languageserver.filemanager.PathWatcherProtocol
|
||||
import org.enso.languageserver.io.InputOutputApi._
|
||||
import org.enso.languageserver.io.OutputKind.{StandardError, StandardOutput}
|
||||
import org.enso.languageserver.io.{InputOutputApi, InputOutputProtocol}
|
||||
import org.enso.languageserver.monitoring.MonitoringApi.Ping
|
||||
import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping}
|
||||
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
|
||||
import org.enso.languageserver.requesthandler._
|
||||
import org.enso.languageserver.requesthandler.capability._
|
||||
import org.enso.languageserver.requesthandler.io._
|
||||
import org.enso.languageserver.requesthandler.monitoring.PingHandler
|
||||
import org.enso.languageserver.requesthandler.monitoring.{
|
||||
InitialPingHandler,
|
||||
PingHandler
|
||||
}
|
||||
import org.enso.languageserver.requesthandler.refactoring.RenameProjectHandler
|
||||
import org.enso.languageserver.requesthandler.session.InitProtocolConnectionHandler
|
||||
import org.enso.languageserver.requesthandler.text._
|
||||
@ -37,18 +40,12 @@ import org.enso.languageserver.requesthandler.visualisation.{
|
||||
}
|
||||
import org.enso.languageserver.runtime.ContextRegistryProtocol
|
||||
import org.enso.languageserver.runtime.ExecutionApi._
|
||||
import org.enso.languageserver.search.SearchApi.{
|
||||
Completion,
|
||||
GetSuggestionsDatabase,
|
||||
GetSuggestionsDatabaseVersion,
|
||||
Import,
|
||||
InvalidateSuggestionsDatabase
|
||||
}
|
||||
import org.enso.languageserver.runtime.VisualisationApi.{
|
||||
AttachVisualisation,
|
||||
DetachVisualisation,
|
||||
ModifyVisualisation
|
||||
}
|
||||
import org.enso.languageserver.search.SearchApi._
|
||||
import org.enso.languageserver.search.{SearchApi, SearchProtocol}
|
||||
import org.enso.languageserver.session.JsonSession
|
||||
import org.enso.languageserver.session.SessionApi.{
|
||||
@ -246,6 +243,7 @@ class JsonConnectionController(
|
||||
),
|
||||
requestTimeout
|
||||
),
|
||||
InitialPing -> InitialPingHandler.props,
|
||||
AcquireCapability -> AcquireCapabilityHandler
|
||||
.props(capabilityRouter, requestTimeout, rpcSession),
|
||||
ReleaseCapability -> ReleaseCapabilityHandler
|
||||
|
@ -10,7 +10,7 @@ import org.enso.languageserver.capability.CapabilityApi.{
|
||||
}
|
||||
import org.enso.languageserver.filemanager.FileManagerApi._
|
||||
import org.enso.languageserver.io.InputOutputApi._
|
||||
import org.enso.languageserver.monitoring.MonitoringApi.Ping
|
||||
import org.enso.languageserver.monitoring.MonitoringApi.{InitialPing, Ping}
|
||||
import org.enso.languageserver.refactoring.RefactoringApi.RenameProject
|
||||
import org.enso.languageserver.runtime.ExecutionApi._
|
||||
import org.enso.languageserver.search.SearchApi._
|
||||
@ -24,6 +24,7 @@ object JsonRpc {
|
||||
*/
|
||||
val protocol: Protocol = Protocol.empty
|
||||
.registerRequest(Ping)
|
||||
.registerRequest(InitialPing)
|
||||
.registerRequest(InitProtocolConnection)
|
||||
.registerRequest(AcquireCapability)
|
||||
.registerRequest(ReleaseCapability)
|
||||
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
@ -29,7 +29,11 @@ class PingHandlerSpec
|
||||
.props(List(subsystem1.ref, subsystem2.ref, subsystem3.ref), 10.seconds)
|
||||
)
|
||||
//when
|
||||
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused)
|
||||
actorUnderTest ! Request(
|
||||
MonitoringApi.Ping,
|
||||
Number(1),
|
||||
Unused
|
||||
)
|
||||
//then
|
||||
subsystem1.expectMsg(Ping)
|
||||
subsystem2.expectMsg(Ping)
|
||||
@ -48,7 +52,11 @@ class PingHandlerSpec
|
||||
.props(List(subsystem1.ref, subsystem2.ref, subsystem3.ref), 10.seconds)
|
||||
)
|
||||
//when
|
||||
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused)
|
||||
actorUnderTest ! Request(
|
||||
MonitoringApi.Ping,
|
||||
Number(1),
|
||||
Unused
|
||||
)
|
||||
subsystem1.expectMsg(Ping)
|
||||
subsystem1.lastSender ! Pong
|
||||
subsystem2.expectMsg(Ping)
|
||||
@ -56,7 +64,9 @@ class PingHandlerSpec
|
||||
subsystem3.expectMsg(Ping)
|
||||
subsystem3.lastSender ! Pong
|
||||
//then
|
||||
expectMsg(ResponseResult(MonitoringApi.Ping, Number(1), Unused))
|
||||
expectMsg(
|
||||
ResponseResult(MonitoringApi.Ping, Number(1), Unused)
|
||||
)
|
||||
//teardown
|
||||
system.stop(actorUnderTest)
|
||||
}
|
||||
@ -72,7 +82,11 @@ class PingHandlerSpec
|
||||
)
|
||||
watch(actorUnderTest)
|
||||
//when
|
||||
actorUnderTest ! Request(MonitoringApi.Ping, Number(1), Unused)
|
||||
actorUnderTest ! Request(
|
||||
MonitoringApi.Ping,
|
||||
Number(1),
|
||||
Unused
|
||||
)
|
||||
subsystem2.expectMsg(Ping)
|
||||
subsystem2.lastSender ! Pong
|
||||
subsystem3.expectMsg(Ping)
|
||||
|
@ -154,6 +154,7 @@ class BaseServerTest extends JsonRpcServerTestKit {
|
||||
|
||||
Await.ready(suggestionsRepoInit, timeout)
|
||||
Await.ready(versionsRepoInit, timeout)
|
||||
system.eventStream.publish(InitializedEvent.InitializationFinished)
|
||||
|
||||
new JsonConnectionControllerFactory(
|
||||
bufferRegistry,
|
||||
|
@ -8,13 +8,13 @@ import org.enso.languageserver.search.Suggestions
|
||||
import org.enso.languageserver.websocket.json.{SearchJsonMessages => json}
|
||||
import org.enso.polyglot.data.Tree
|
||||
import org.enso.polyglot.runtime.Runtime.Api
|
||||
import org.enso.testkit.FlakySpec
|
||||
import org.enso.testkit.RetrySpec
|
||||
|
||||
class SuggestionsHandlerEventsTest extends BaseServerTest with FlakySpec {
|
||||
class SuggestionsHandlerEventsTest extends BaseServerTest with RetrySpec {
|
||||
|
||||
"SuggestionsHandlerEvents" must {
|
||||
|
||||
"send suggestions database notifications" taggedAs Flaky in {
|
||||
"send suggestions database notifications" taggedAs Retry in {
|
||||
val client = getInitialisedWsClient()
|
||||
system.eventStream.publish(ProjectNameChangedEvent("Test", "Test"))
|
||||
|
||||
|
@ -81,7 +81,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
.newProject(
|
||||
path = actualPath,
|
||||
name = name,
|
||||
version = version,
|
||||
engineVersion = version,
|
||||
authorName = globalConfig.authorName,
|
||||
authorEmail = globalConfig.authorEmail,
|
||||
additionalArguments = additionalArguments
|
||||
@ -414,7 +414,9 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
val (runtimeVersionRunSettings, whichEngine) = runner.version(useJSON).get
|
||||
|
||||
val isEngineInstalled =
|
||||
componentsManager.findEngine(runtimeVersionRunSettings.version).isDefined
|
||||
componentsManager
|
||||
.findEngine(runtimeVersionRunSettings.engineVersion)
|
||||
.isDefined
|
||||
val runtimeVersionString = if (isEngineInstalled) {
|
||||
val output = runner.withCommand(
|
||||
runtimeVersionRunSettings,
|
||||
|
@ -120,14 +120,14 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
.newProject(
|
||||
path = projectPath,
|
||||
name = "ProjectName",
|
||||
version = defaultEngineVersion,
|
||||
engineVersion = defaultEngineVersion,
|
||||
authorName = Some(authorName),
|
||||
authorEmail = Some(authorEmail),
|
||||
additionalArguments = Seq(additionalArgument)
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual defaultEngineVersion
|
||||
runSettings.engineVersion shouldEqual defaultEngineVersion
|
||||
runSettings.runnerArguments should contain(additionalArgument)
|
||||
val commandLine = runSettings.runnerArguments.mkString(" ")
|
||||
commandLine should include(
|
||||
@ -149,7 +149,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual defaultEngineVersion
|
||||
runSettings.engineVersion shouldEqual defaultEngineVersion
|
||||
runSettings.runnerArguments should (contain("arg") and contain("--flag"))
|
||||
runSettings.runnerArguments.mkString(" ") should
|
||||
(include("--repl") and not include s"--in-project")
|
||||
@ -174,7 +174,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
outsideProject.version shouldEqual version
|
||||
outsideProject.engineVersion shouldEqual version
|
||||
outsideProject.runnerArguments.mkString(" ") should
|
||||
(include(s"--in-project $normalizedPath") and include("--repl"))
|
||||
|
||||
@ -188,7 +188,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
insideProject.version shouldEqual version
|
||||
insideProject.engineVersion shouldEqual version
|
||||
insideProject.runnerArguments.mkString(" ") should
|
||||
(include(s"--in-project $normalizedPath") and include("--repl"))
|
||||
|
||||
@ -202,7 +202,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
overriddenRun.version shouldEqual overridden
|
||||
overriddenRun.engineVersion shouldEqual overridden
|
||||
overriddenRun.runnerArguments.mkString(" ") should
|
||||
(include(s"--in-project $normalizedPath") and include("--repl"))
|
||||
}
|
||||
@ -230,7 +230,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual version
|
||||
runSettings.engineVersion shouldEqual version
|
||||
val commandLine = runSettings.runnerArguments.mkString(" ")
|
||||
commandLine should include(s"--interface ${options.interface}")
|
||||
commandLine should include(s"--rpc-port ${options.rpcPort}")
|
||||
@ -250,7 +250,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
.version shouldEqual overridden
|
||||
.engineVersion shouldEqual overridden
|
||||
}
|
||||
|
||||
"run a project" in {
|
||||
@ -270,7 +270,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
outsideProject.version shouldEqual version
|
||||
outsideProject.engineVersion shouldEqual version
|
||||
outsideProject.runnerArguments.mkString(" ") should
|
||||
include(s"--run $normalizedPath")
|
||||
|
||||
@ -284,7 +284,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
insideProject.version shouldEqual version
|
||||
insideProject.engineVersion shouldEqual version
|
||||
insideProject.runnerArguments.mkString(" ") should
|
||||
include(s"--run $normalizedPath")
|
||||
|
||||
@ -298,7 +298,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
overriddenRun.version shouldEqual overridden
|
||||
overriddenRun.engineVersion shouldEqual overridden
|
||||
overriddenRun.runnerArguments.mkString(" ") should
|
||||
include(s"--run $normalizedPath")
|
||||
|
||||
@ -338,7 +338,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual defaultEngineVersion
|
||||
runSettings.engineVersion shouldEqual defaultEngineVersion
|
||||
runSettings.runnerArguments.mkString(" ") should
|
||||
(include(s"--run $normalizedPath") and (not(include("--in-project"))))
|
||||
}
|
||||
@ -362,7 +362,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual version
|
||||
runSettings.engineVersion shouldEqual version
|
||||
runSettings.runnerArguments.mkString(" ") should
|
||||
(include(s"--run $normalizedFilePath") and
|
||||
include(s"--in-project $normalizedProjectPath"))
|
||||
@ -374,7 +374,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
.version(useJSON = true)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual defaultEngineVersion
|
||||
runSettings.engineVersion shouldEqual defaultEngineVersion
|
||||
runSettings.runnerArguments should
|
||||
(contain("--version") and contain("--json"))
|
||||
|
||||
@ -391,7 +391,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
|
||||
.version(useJSON = false)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual version
|
||||
runSettings.engineVersion shouldEqual version
|
||||
runSettings.runnerArguments should
|
||||
(contain("--version") and not(contain("--json")))
|
||||
|
||||
|
@ -72,7 +72,7 @@ class UpgradeSpec
|
||||
*
|
||||
* If `launcherVersion` is not provided, the default one is used.
|
||||
*
|
||||
* It waits a 100ms delay after creating the launcher copy to ensure that the
|
||||
* It waits a 250ms delay after creating the launcher copy to ensure that the
|
||||
* copy can be called right away after calling this function. It is not
|
||||
* absolutely certain that this is helpful, but from time to time, the tests
|
||||
* fail because the filesystem does not allow to access the executable as
|
||||
@ -94,7 +94,7 @@ class UpgradeSpec
|
||||
val root = launcherPath.getParent.getParent
|
||||
FileSystem.writeTextFile(root / ".enso.portable", "mark")
|
||||
}
|
||||
Thread.sleep(100)
|
||||
Thread.sleep(250)
|
||||
}
|
||||
|
||||
/** Path to the launcher executable in the temporary distribution.
|
||||
@ -156,7 +156,7 @@ class UpgradeSpec
|
||||
}
|
||||
|
||||
"upgrade" should {
|
||||
"upgrade to latest version (excluding broken)" in {
|
||||
"upgrade to latest version (excluding broken)" taggedAs Retry in {
|
||||
prepareDistribution(
|
||||
portable = true,
|
||||
launcherVersion = Some(SemVer(0, 0, 2))
|
||||
@ -166,7 +166,7 @@ class UpgradeSpec
|
||||
checkVersion() shouldEqual SemVer(0, 0, 4)
|
||||
}
|
||||
|
||||
"not downgrade without being explicitly asked to do so" in {
|
||||
"not downgrade without being explicitly asked to do so" taggedAs Retry in {
|
||||
// precondition for the test to make sense
|
||||
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
|
||||
|
||||
@ -177,7 +177,7 @@ class UpgradeSpec
|
||||
}
|
||||
|
||||
"upgrade/downgrade to a specific version " +
|
||||
"(and update necessary files)" in {
|
||||
"(and update necessary files)" taggedAs Retry in {
|
||||
// precondition for the test to make sense
|
||||
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
|
||||
|
||||
@ -194,7 +194,7 @@ class UpgradeSpec
|
||||
.trim shouldEqual "Test license"
|
||||
}
|
||||
|
||||
"upgrade also in installed mode" in {
|
||||
"upgrade also in installed mode" taggedAs Retry in {
|
||||
prepareDistribution(
|
||||
portable = false,
|
||||
launcherVersion = Some(SemVer(0, 0, 0))
|
||||
@ -228,37 +228,46 @@ class UpgradeSpec
|
||||
)
|
||||
|
||||
checkVersion() shouldEqual SemVer(0, 0, 0)
|
||||
run(Seq("upgrade", "0.0.3")) should returnSuccess
|
||||
val process = startLauncher(Seq("upgrade", "0.0.3"))
|
||||
try {
|
||||
process.join(timeoutSeconds = 30) should returnSuccess
|
||||
|
||||
checkVersion() shouldEqual SemVer(0, 0, 3)
|
||||
checkVersion() shouldEqual SemVer(0, 0, 3)
|
||||
|
||||
val launchedVersions = Seq(
|
||||
"0.0.0",
|
||||
"0.0.0",
|
||||
"0.0.1",
|
||||
"0.0.2",
|
||||
"0.0.3"
|
||||
)
|
||||
val launchedVersions = Seq(
|
||||
"0.0.0",
|
||||
"0.0.0",
|
||||
"0.0.1",
|
||||
"0.0.2",
|
||||
"0.0.3"
|
||||
)
|
||||
|
||||
val reportedLaunchLog = TestHelpers
|
||||
.readFileContent(launcherPath.getParent / ".launcher_version_log")
|
||||
.trim
|
||||
.linesIterator
|
||||
.toSeq
|
||||
val reportedLaunchLog = TestHelpers
|
||||
.readFileContent(launcherPath.getParent / ".launcher_version_log")
|
||||
.trim
|
||||
.linesIterator
|
||||
.toSeq
|
||||
|
||||
reportedLaunchLog shouldEqual launchedVersions
|
||||
reportedLaunchLog shouldEqual launchedVersions
|
||||
|
||||
withClue(
|
||||
"After the update we run the version check, running the launcher " +
|
||||
"after the update should ensure no leftover temporary executables " +
|
||||
"are left in the bin directory."
|
||||
) {
|
||||
val binDirectory = launcherPath.getParent
|
||||
val leftOverExecutables = FileSystem
|
||||
.listDirectory(binDirectory)
|
||||
.map(_.getFileName.toString)
|
||||
.filter(_.startsWith("enso"))
|
||||
leftOverExecutables shouldEqual Seq(OS.executableName("enso"))
|
||||
withClue(
|
||||
"After the update we run the version check, running the launcher " +
|
||||
"after the update should ensure no leftover temporary executables " +
|
||||
"are left in the bin directory."
|
||||
) {
|
||||
val binDirectory = launcherPath.getParent
|
||||
val leftOverExecutables = FileSystem
|
||||
.listDirectory(binDirectory)
|
||||
.map(_.getFileName.toString)
|
||||
.filter(_.startsWith("enso"))
|
||||
leftOverExecutables shouldEqual Seq(OS.executableName("enso"))
|
||||
}
|
||||
} finally {
|
||||
if (process.isAlive) {
|
||||
// ensure that the child process frees resources if retrying the test
|
||||
process.kill()
|
||||
Thread.sleep(500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,8 +280,8 @@ class UpgradeSpec
|
||||
val enginesPath = getTestDirectory / "enso" / "dist"
|
||||
Files.createDirectories(enginesPath)
|
||||
|
||||
// TODO [RW] re-enable this test when #1046 is done and the engine
|
||||
// distribution can be used in the test
|
||||
// TODO [RW] re-enable this test when #1046 or #1273 is done and the
|
||||
// engine distribution can be used in the test
|
||||
// FileSystem.copyDirectory(
|
||||
// Path.of("target/distribution/"),
|
||||
// enginesPath / "0.1.0"
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import org.enso.languageserver.boot.{
|
||||
LanguageServerComponent,
|
||||
LanguageServerConfig
|
||||
}
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
@ -16,10 +17,11 @@ object LanguageServerApp {
|
||||
/** Runs a Language Server
|
||||
*
|
||||
* @param config a config
|
||||
* @param logLevel log level
|
||||
*/
|
||||
def run(config: LanguageServerConfig): Unit = {
|
||||
def run(config: LanguageServerConfig, logLevel: LogLevel): Unit = {
|
||||
println("Starting Language Server...")
|
||||
val server = new LanguageServerComponent(config)
|
||||
val server = new LanguageServerComponent(config, logLevel)
|
||||
Await.result(server.start(), 10.seconds)
|
||||
StdIn.readLine()
|
||||
Await.result(server.stop(), 10.seconds)
|
||||
|
@ -5,7 +5,6 @@ import java.util.UUID
|
||||
|
||||
import akka.http.scaladsl.model.{IllegalUriException, Uri}
|
||||
import cats.implicits._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.apache.commons.cli.{Option => CliOption, _}
|
||||
import org.enso.languageserver.boot
|
||||
import org.enso.languageserver.boot.LanguageServerConfig
|
||||
@ -234,19 +233,13 @@ object Main {
|
||||
): Unit = {
|
||||
val root = new File(path)
|
||||
val name = nameOption.getOrElse(PackageManager.Default.generateName(root))
|
||||
val currentVersion = SemVer(buildinfo.Info.ensoVersion).getOrElse {
|
||||
throw new IllegalStateException(
|
||||
"Fatal error: Enso version included in buildinfo is not a valid " +
|
||||
"semver string, this should never happen."
|
||||
)
|
||||
}
|
||||
val authors =
|
||||
if (authorName.isEmpty && authorEmail.isEmpty) List()
|
||||
else List(Contact(name = authorName, email = authorEmail))
|
||||
PackageManager.Default.create(
|
||||
root = root,
|
||||
name = name,
|
||||
ensoVersion = SemVerEnsoVersion(currentVersion),
|
||||
ensoVersion = SemVerEnsoVersion(CurrentVersion.version),
|
||||
authors = authors,
|
||||
maintainers = authors
|
||||
)
|
||||
@ -432,8 +425,6 @@ object Main {
|
||||
* @param logLevel log level to set for the engine runtime
|
||||
*/
|
||||
private def runLanguageServer(line: CommandLine, logLevel: LogLevel): Unit = {
|
||||
val _ = logLevel // TODO [RW] handle logging in the Language Server (#1144)
|
||||
|
||||
val maybeConfig = parseSeverOptions(line)
|
||||
|
||||
maybeConfig match {
|
||||
@ -442,7 +433,7 @@ object Main {
|
||||
exitFail()
|
||||
|
||||
case Right(config) =>
|
||||
LanguageServerApp.run(config)
|
||||
LanguageServerApp.run(config, logLevel)
|
||||
exitSuccess()
|
||||
}
|
||||
}
|
||||
@ -481,7 +472,8 @@ object Main {
|
||||
def displayVersion(useJson: Boolean): Unit = {
|
||||
val versionDescription = VersionDescription.make(
|
||||
"Enso Compiler and Runtime",
|
||||
includeRuntimeJVMInfo = true
|
||||
includeRuntimeJVMInfo = true,
|
||||
customVersion = Some(CurrentVersion.version.toString)
|
||||
)
|
||||
println(versionDescription.asString(useJson))
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ abstract class JsonRpcServerTestKit
|
||||
val _ = binding.unbind()
|
||||
}
|
||||
|
||||
class WsTestClient(address: String) {
|
||||
class WsTestClient(address: String, debugMessages: Boolean = false) {
|
||||
private var inActor: ActorRef = _
|
||||
private val outActor: TestProbe = TestProbe()
|
||||
private val source: Source[Message, NotUsed] = Source
|
||||
@ -104,8 +104,11 @@ abstract class JsonRpcServerTestKit
|
||||
|
||||
def send(json: Json): Unit = send(json.noSpaces)
|
||||
|
||||
def expectMessage(timeout: FiniteDuration = 3.seconds.dilated): String =
|
||||
outActor.expectMsgClass[String](timeout, classOf[String])
|
||||
def expectMessage(timeout: FiniteDuration = 3.seconds.dilated): String = {
|
||||
val message = outActor.expectMsgClass[String](timeout, classOf[String])
|
||||
if (debugMessages) println(message)
|
||||
message
|
||||
}
|
||||
|
||||
def expectJson(
|
||||
json: Json,
|
||||
|
@ -15,6 +15,7 @@ sealed abstract class LogLevel(final val level: Int) {
|
||||
def shouldLog(other: LogLevel): Boolean =
|
||||
other.level <= level
|
||||
}
|
||||
|
||||
object LogLevel {
|
||||
|
||||
/** This log level should not be used by messages, instead it can be set as
|
||||
@ -93,19 +94,38 @@ object LogLevel {
|
||||
level.level.asJson
|
||||
}
|
||||
|
||||
/** Creates a [[LogLevel]] from its integer representation.
|
||||
*
|
||||
* Returns None if the number does not represent a valid log level.
|
||||
*/
|
||||
def fromInteger(level: Int): Option[LogLevel] = level match {
|
||||
case Error.level => Some(Error)
|
||||
case Warning.level => Some(Warning)
|
||||
case Info.level => Some(Info)
|
||||
case Debug.level => Some(Debug)
|
||||
case Trace.level => Some(Trace)
|
||||
case _ => None
|
||||
}
|
||||
|
||||
/** [[Decoder]] instance for [[LogLevel]].
|
||||
*/
|
||||
implicit val decoder: Decoder[LogLevel] = { json =>
|
||||
json.as[Int].flatMap {
|
||||
case Error.level => Right(Error)
|
||||
case Warning.level => Right(Warning)
|
||||
case Info.level => Right(Info)
|
||||
case Debug.level => Right(Debug)
|
||||
case Trace.level => Right(Trace)
|
||||
case other =>
|
||||
Left(
|
||||
DecodingFailure(s"`$other` is not a valid log level.", json.history)
|
||||
)
|
||||
json.as[Int].flatMap { level =>
|
||||
fromInteger(level).toRight(
|
||||
DecodingFailure(s"`$level` is not a valid log level.", json.history)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Converts our internal [[LogLevel]] to the corresponding instance of
|
||||
* Akka-specific log level.
|
||||
*/
|
||||
def toAkka(logLevel: LogLevel): akka.event.Logging.LogLevel = logLevel match {
|
||||
case Off => akka.event.Logging.LogLevel(Int.MinValue)
|
||||
case Error => akka.event.Logging.ErrorLevel
|
||||
case Warning => akka.event.Logging.WarningLevel
|
||||
case Info => akka.event.Logging.InfoLevel
|
||||
case Debug => akka.event.Logging.DebugLevel
|
||||
case Trace => akka.event.Logging.DebugLevel
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,7 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import org.enso.loggingservice.internal.service.{Client, Local, Server, Service}
|
||||
import org.enso.loggingservice.internal.{
|
||||
BlockingConsumerMessageQueue,
|
||||
InternalLogMessage,
|
||||
InternalLogger,
|
||||
LoggerConnection
|
||||
}
|
||||
import org.enso.loggingservice.internal._
|
||||
import org.enso.loggingservice.printers.{Printer, StderrPrinter}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -14,8 +9,37 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
/** Manages the logging service.
|
||||
*/
|
||||
object LoggingServiceManager {
|
||||
private val messageQueue = new BlockingConsumerMessageQueue()
|
||||
private var currentLevel: LogLevel = LogLevel.Trace
|
||||
private val testLoggingPropertyKey = "org.enso.loggingservice.test-log-level"
|
||||
|
||||
private var currentService: Option[Service] = None
|
||||
private var currentLevel: LogLevel = LogLevel.Trace
|
||||
|
||||
/** Creates an instance for the [[messageQueue]].
|
||||
*
|
||||
* Runs special workaround logic if test mode is detected.
|
||||
*/
|
||||
private def initializeMessageQueue(): BlockingConsumerMessageQueue = {
|
||||
sys.props.get(testLoggingPropertyKey) match {
|
||||
case Some(value) =>
|
||||
val logLevel =
|
||||
value.toIntOption.flatMap(LogLevel.fromInteger).getOrElse {
|
||||
System.err.println(
|
||||
s"Invalid log level for $testLoggingPropertyKey, " +
|
||||
s"falling back to info."
|
||||
)
|
||||
LogLevel.Info
|
||||
}
|
||||
|
||||
val shouldOverride = () => currentService.isEmpty
|
||||
|
||||
new TestMessageQueue(logLevel, shouldOverride)
|
||||
case None => productionMessageQueue()
|
||||
}
|
||||
}
|
||||
|
||||
private def productionMessageQueue() = new BlockingConsumerMessageQueue()
|
||||
|
||||
private val messageQueue = initializeMessageQueue()
|
||||
|
||||
/** The default [[LoggerConnection]] that should be used by all backends which
|
||||
* want to use the logging service.
|
||||
@ -32,8 +56,6 @@ object LoggingServiceManager {
|
||||
override def logLevel: LogLevel = currentLevel
|
||||
}
|
||||
|
||||
private var currentService: Option[Service] = None
|
||||
|
||||
/** Sets up the logging service, but in a separate thread to avoid stalling
|
||||
* the application.
|
||||
*
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import org.enso.projectmanager.control.core.{Applicative, CovariantFlatMap}
|
||||
import org.enso.projectmanager.control.effect.{Async, ErrorChannel, Exec, Sync}
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.languageserver.{
|
||||
ExecutorWithUnlimitedPool,
|
||||
LanguageServerGatewayImpl,
|
||||
LanguageServerRegistry,
|
||||
ShutdownHookActivator
|
||||
@ -25,6 +26,7 @@ import org.enso.projectmanager.service.config.GlobalConfigService
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService
|
||||
import org.enso.projectmanager.service.{
|
||||
MonadicProjectValidator,
|
||||
ProjectCreationService,
|
||||
ProjectService,
|
||||
ProjectServiceFailure,
|
||||
ValidationFailure
|
||||
@ -76,7 +78,9 @@ class MainModule[
|
||||
config.network,
|
||||
config.bootloader,
|
||||
config.supervision,
|
||||
config.timeout
|
||||
config.timeout,
|
||||
DefaultDistributionConfiguration,
|
||||
ExecutorWithUnlimitedPool
|
||||
),
|
||||
"language-server-registry"
|
||||
)
|
||||
@ -94,19 +98,25 @@ class MainModule[
|
||||
config.timeout
|
||||
)
|
||||
|
||||
lazy val projectCreationService =
|
||||
new ProjectCreationService[F](DefaultDistributionConfiguration)
|
||||
|
||||
lazy val globalConfigService =
|
||||
new GlobalConfigService[F](DefaultDistributionConfiguration)
|
||||
|
||||
lazy val projectService =
|
||||
new ProjectService[F](
|
||||
projectValidator,
|
||||
projectRepository,
|
||||
projectCreationService,
|
||||
globalConfigService,
|
||||
logging,
|
||||
clock,
|
||||
gen,
|
||||
languageServerGateway
|
||||
languageServerGateway,
|
||||
DefaultDistributionConfiguration
|
||||
)
|
||||
|
||||
lazy val globalConfigService =
|
||||
new GlobalConfigService[F](DefaultDistributionConfiguration)
|
||||
|
||||
lazy val runtimeVersionManagementService =
|
||||
new RuntimeVersionManagementService[F](DefaultDistributionConfiguration)
|
||||
|
||||
|
@ -8,7 +8,10 @@ import akka.pattern.pipe
|
||||
import akka.stream.scaladsl.{Flow, Sink, Source}
|
||||
import akka.stream.{CompletionStrategy, OverflowStrategy}
|
||||
import org.enso.projectmanager.infrastructure.http.AkkaBasedWebSocketConnection._
|
||||
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.Listen
|
||||
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.{
|
||||
Attach,
|
||||
Detach
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.http.WebSocketConnection.{
|
||||
WebSocketConnected,
|
||||
WebSocketMessage,
|
||||
@ -65,7 +68,11 @@ class AkkaBasedWebSocketConnection(address: String)(implicit
|
||||
|
||||
/** @inheritdoc */
|
||||
override def attachListener(listener: ActorRef): Unit =
|
||||
receiver ! Listen(listener)
|
||||
receiver ! Attach(listener)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def detachListener(listener: ActorRef): Unit =
|
||||
receiver ! Detach(listener)
|
||||
|
||||
/** @inheritdoc */
|
||||
def connect(): Unit = {
|
||||
@ -83,6 +90,7 @@ class AkkaBasedWebSocketConnection(address: String)(implicit
|
||||
case InvalidUpgradeResponse(_, cause) =>
|
||||
WebSocketStreamFailure(new Exception(s"Cannot connect $cause"))
|
||||
}
|
||||
.recover(WebSocketStreamFailure(_))
|
||||
.pipeTo(receiver)
|
||||
()
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
package org.enso.projectmanager.infrastructure.http
|
||||
|
||||
import akka.actor.{Actor, ActorRef}
|
||||
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.Listen
|
||||
import org.enso.projectmanager.infrastructure.http.FanOutReceiver.{
|
||||
Attach,
|
||||
Detach
|
||||
}
|
||||
|
||||
/** A fan-out receiver that delivers messages to multiple listeners.
|
||||
*/
|
||||
@ -10,7 +13,8 @@ class FanOutReceiver extends Actor {
|
||||
override def receive: Receive = running()
|
||||
|
||||
private def running(listeners: Set[ActorRef] = Set.empty): Receive = {
|
||||
case Listen(listener) => context.become(running(listeners + listener))
|
||||
case Attach(listener) => context.become(running(listeners + listener))
|
||||
case Detach(listener) => context.become(running(listeners - listener))
|
||||
case msg => listeners.foreach(_ ! msg)
|
||||
}
|
||||
|
||||
@ -22,6 +26,6 @@ object FanOutReceiver {
|
||||
*
|
||||
* @param listener a listener to attach
|
||||
*/
|
||||
case class Listen(listener: ActorRef)
|
||||
|
||||
case class Attach(listener: ActorRef)
|
||||
case class Detach(listener: ActorRef)
|
||||
}
|
||||
|
@ -26,6 +26,12 @@ trait WebSocketConnection {
|
||||
*/
|
||||
def attachListener(listener: ActorRef): Unit
|
||||
|
||||
/** Removes the listener of incoming messages.
|
||||
*
|
||||
* Can be useful when disconnecting has timed out and we do not want to
|
||||
* receive the disconnected message after the owning actor is long dead.
|
||||
*/
|
||||
def detachListener(listener: ActorRef): Unit
|
||||
}
|
||||
|
||||
object WebSocketConnection {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,10 @@ import org.enso.projectmanager.infrastructure.languageserver.HeartbeatSession.{
|
||||
HeartbeatTimeout,
|
||||
SocketClosureTimeout
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerSupervisor.ServerUnresponsive
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerSupervisor.{
|
||||
HeartbeatReceived,
|
||||
ServerUnresponsive
|
||||
}
|
||||
import org.enso.projectmanager.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
@ -28,12 +31,21 @@ import scala.concurrent.duration.FiniteDuration
|
||||
* @param timeout a session timeout
|
||||
* @param connectionFactory a web socket connection factory
|
||||
* @param scheduler a scheduler
|
||||
* @param method api method to use for the heartbeat message
|
||||
* @param sendConfirmations whether to send [[HeartbeatReceived]] to confirm
|
||||
* that a response has been received
|
||||
* @param quietErrors if set, reports errors in debug level instead of error
|
||||
* level, can be used when errors are expected (i.e. on
|
||||
* startup)
|
||||
*/
|
||||
class HeartbeatSession(
|
||||
socket: Socket,
|
||||
timeout: FiniteDuration,
|
||||
connectionFactory: WebSocketConnectionFactory,
|
||||
scheduler: Scheduler
|
||||
scheduler: Scheduler,
|
||||
method: String,
|
||||
sendConfirmations: Boolean,
|
||||
quietErrors: Boolean
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
@ -57,7 +69,7 @@ class HeartbeatSession(
|
||||
connection.send(s"""
|
||||
|{
|
||||
| "jsonrpc": "2.0",
|
||||
| "method": "heartbeat/ping",
|
||||
| "method": "$method",
|
||||
| "id": "$requestId",
|
||||
| "params": null
|
||||
|}
|
||||
@ -66,7 +78,7 @@ class HeartbeatSession(
|
||||
context.become(pongStage(cancellable))
|
||||
|
||||
case WebSocketStreamFailure(th) =>
|
||||
log.error(th, s"An error occurred during connecting to websocket $socket")
|
||||
logError(th, s"An error occurred during connecting to websocket $socket")
|
||||
context.parent ! ServerUnresponsive
|
||||
stop()
|
||||
|
||||
@ -81,16 +93,18 @@ class HeartbeatSession(
|
||||
|
||||
maybeJson match {
|
||||
case Left(error) =>
|
||||
log.error(error, "An error occurred during parsing pong reply")
|
||||
logError(error, "An error occurred during parsing pong reply")
|
||||
|
||||
case Right(id) =>
|
||||
if (id == requestId.toString) {
|
||||
log.debug(s"Received correct pong message from $socket")
|
||||
|
||||
if (sendConfirmations) {
|
||||
context.parent ! HeartbeatReceived
|
||||
}
|
||||
|
||||
cancellable.cancel()
|
||||
connection.disconnect()
|
||||
val closureTimeout =
|
||||
scheduler.scheduleOnce(timeout, self, SocketClosureTimeout)
|
||||
context.become(socketClosureStage(closureTimeout))
|
||||
stop()
|
||||
} else {
|
||||
log.warning(s"Received unknown response $payload")
|
||||
}
|
||||
@ -99,21 +113,17 @@ class HeartbeatSession(
|
||||
case HeartbeatTimeout =>
|
||||
log.debug(s"Heartbeat timeout detected for $requestId")
|
||||
context.parent ! ServerUnresponsive
|
||||
connection.disconnect()
|
||||
val closureTimeout =
|
||||
scheduler.scheduleOnce(timeout, self, SocketClosureTimeout)
|
||||
context.become(socketClosureStage(closureTimeout))
|
||||
stop()
|
||||
|
||||
case WebSocketStreamClosed =>
|
||||
context.parent ! ServerUnresponsive
|
||||
context.stop(self)
|
||||
|
||||
case WebSocketStreamFailure(th) =>
|
||||
log.error(th, s"An error occurred during waiting for Pong message")
|
||||
logError(th, s"An error occurred during waiting for Pong message")
|
||||
context.parent ! ServerUnresponsive
|
||||
cancellable.cancel()
|
||||
connection.disconnect()
|
||||
context.stop(self)
|
||||
stop()
|
||||
|
||||
case GracefulStop =>
|
||||
cancellable.cancel()
|
||||
@ -126,13 +136,14 @@ class HeartbeatSession(
|
||||
cancellable.cancel()
|
||||
|
||||
case WebSocketStreamFailure(th) =>
|
||||
log.error(th, s"An error occurred during closing web socket")
|
||||
logError(th, s"An error occurred during closing web socket")
|
||||
context.stop(self)
|
||||
cancellable.cancel()
|
||||
|
||||
case SocketClosureTimeout =>
|
||||
log.error(s"Socket closure timed out")
|
||||
logError(s"Socket closure timed out")
|
||||
context.stop(self)
|
||||
connection.detachListener(self)
|
||||
|
||||
case GracefulStop => // ignoring it, because the actor is already closing
|
||||
}
|
||||
@ -144,6 +155,22 @@ class HeartbeatSession(
|
||||
context.become(socketClosureStage(closureTimeout))
|
||||
}
|
||||
|
||||
private def logError(throwable: Throwable, message: String): Unit = {
|
||||
if (quietErrors) {
|
||||
log.debug(s"$message ($throwable)")
|
||||
} else {
|
||||
log.error(throwable, message)
|
||||
}
|
||||
}
|
||||
|
||||
private def logError(message: String): Unit = {
|
||||
if (quietErrors) {
|
||||
log.debug(message)
|
||||
} else {
|
||||
log.error(message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object HeartbeatSession {
|
||||
@ -156,7 +183,8 @@ object HeartbeatSession {
|
||||
*/
|
||||
case object SocketClosureTimeout
|
||||
|
||||
/** Creates a configuration object used to create a [[LanguageServerSupervisor]].
|
||||
/** Creates a configuration object used to create an ordinary
|
||||
* [[HeartbeatSession]] for monitoring server's status.
|
||||
*
|
||||
* @param socket a server socket
|
||||
* @param timeout a session timeout
|
||||
@ -170,6 +198,43 @@ object HeartbeatSession {
|
||||
connectionFactory: WebSocketConnectionFactory,
|
||||
scheduler: Scheduler
|
||||
): Props =
|
||||
Props(new HeartbeatSession(socket, timeout, connectionFactory, scheduler))
|
||||
Props(
|
||||
new HeartbeatSession(
|
||||
socket,
|
||||
timeout,
|
||||
connectionFactory,
|
||||
scheduler,
|
||||
"heartbeat/ping",
|
||||
sendConfirmations = false,
|
||||
quietErrors = false
|
||||
)
|
||||
)
|
||||
|
||||
/** Creates a configuration object used to create an initial
|
||||
* [[HeartbeatSession]] for checking if the server has finished booting.
|
||||
*
|
||||
* @param socket a server socket
|
||||
* @param timeout a session timeout
|
||||
* @param connectionFactory a web socket connection factory
|
||||
* @param scheduler a scheduler
|
||||
* @return a configuration object
|
||||
*/
|
||||
def initialProps(
|
||||
socket: Socket,
|
||||
timeout: FiniteDuration,
|
||||
connectionFactory: WebSocketConnectionFactory,
|
||||
scheduler: Scheduler
|
||||
): Props =
|
||||
Props(
|
||||
new HeartbeatSession(
|
||||
socket,
|
||||
timeout,
|
||||
connectionFactory,
|
||||
scheduler,
|
||||
"heartbeat/init",
|
||||
sendConfirmations = true,
|
||||
quietErrors = true
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -1,35 +1,52 @@
|
||||
package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import akka.actor.Status.Failure
|
||||
import akka.actor.{Actor, ActorLogging, Props}
|
||||
import akka.pattern.pipe
|
||||
import org.enso.languageserver.boot.{
|
||||
LanguageServerComponent,
|
||||
LanguageServerConfig
|
||||
}
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||
import org.enso.projectmanager.boot.configuration.BootloaderConfig
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{
|
||||
Boot,
|
||||
FindFreeSocket,
|
||||
ServerBootFailed,
|
||||
ServerBooted
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.net.Tcp
|
||||
import org.enso.projectmanager.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/** It boots a Language Sever described by the `descriptor`. Upon boot failure
|
||||
* looks up new available port and retries to boot the server.
|
||||
*
|
||||
* Once the server is booted it can restart it on request.
|
||||
*
|
||||
* The `bootProgressTracker` has to be provided because, while the process
|
||||
* assumes that the required engine version has been pre-installed, it still
|
||||
* may sometimes need to wait on a lock (for example if an engine is being
|
||||
* uninstalled), so these events need to be reported to the progress tracker.
|
||||
*
|
||||
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
|
||||
* related to initializing the engine
|
||||
* @param descriptor a LS descriptor
|
||||
* @param config a bootloader config
|
||||
* @param bootTimeout a finite duration that determines the deadline for
|
||||
* initialization of the server process, if the process
|
||||
* starts but fails to initialize after this timeout, it is
|
||||
* treated as a boot failure and the process is gracefully
|
||||
* stopped
|
||||
* @param executor an executor service used to start the language server
|
||||
* process
|
||||
*/
|
||||
class LanguageServerBootLoader(
|
||||
bootProgressTracker: ActorRef,
|
||||
descriptor: LanguageServerDescriptor,
|
||||
config: BootloaderConfig
|
||||
config: BootloaderConfig,
|
||||
bootTimeout: FiniteDuration,
|
||||
executor: LanguageServerExecutor
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
|
||||
// TODO [RW] consider adding a stop timeout so that if the graceful stop on
|
||||
// timed-out boot also does not work, the process can be killed forcibly
|
||||
// (#1315)
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
override def preStart(): Unit = {
|
||||
@ -39,6 +56,10 @@ class LanguageServerBootLoader(
|
||||
|
||||
override def receive: Receive = findingSocket()
|
||||
|
||||
/** First bootloader phase - looking for a free set of ports for the server.
|
||||
*
|
||||
* Once the ports are found, the process starts booting.
|
||||
*/
|
||||
private def findingSocket(retry: Int = 0): Receive = {
|
||||
case FindFreeSocket =>
|
||||
log.debug("Looking for available socket to bind the language server")
|
||||
@ -53,53 +74,190 @@ class LanguageServerBootLoader(
|
||||
s"binary:${descriptor.networkConfig.interface}:$binaryPort]"
|
||||
)
|
||||
self ! Boot
|
||||
context.become(booting(jsonRpcPort, binaryPort, retry))
|
||||
context.become(
|
||||
bootingFirstTime(
|
||||
rpcPort = jsonRpcPort,
|
||||
dataPort = binaryPort,
|
||||
retryCount = retry
|
||||
)
|
||||
)
|
||||
|
||||
case GracefulStop =>
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
private def booting(rpcPort: Int, dataPort: Int, retryCount: Int): Receive = {
|
||||
/** This phase is triggered when the ports are found.
|
||||
*
|
||||
* When booting for the first time, the actor that will be notified is the
|
||||
* parent and retries are allowed (upon retry new set of ports will be tried).
|
||||
*/
|
||||
private def bootingFirstTime(
|
||||
rpcPort: Int,
|
||||
dataPort: Int,
|
||||
retryCount: Int
|
||||
): Receive = booting(
|
||||
rpcPort = rpcPort,
|
||||
dataPort = dataPort,
|
||||
shouldRetry = true,
|
||||
retryCount = retryCount,
|
||||
bootRequester = context.parent
|
||||
)
|
||||
|
||||
/** A general booting phase.
|
||||
*
|
||||
* It spawns the [[LanguageServerProcess]] child actor and waits for it to
|
||||
* confirm that the server has been successfully initialized (or failed to
|
||||
* boot). Once booted it enters the running phase.
|
||||
*/
|
||||
private def booting(
|
||||
rpcPort: Int,
|
||||
dataPort: Int,
|
||||
shouldRetry: Boolean,
|
||||
retryCount: Int,
|
||||
bootRequester: ActorRef
|
||||
): Receive = {
|
||||
case Boot =>
|
||||
log.debug("Booting a language server")
|
||||
val config = LanguageServerConfig(
|
||||
descriptor.networkConfig.interface,
|
||||
rpcPort,
|
||||
dataPort,
|
||||
descriptor.rootId,
|
||||
descriptor.root,
|
||||
descriptor.name,
|
||||
context.dispatcher
|
||||
context.actorOf(
|
||||
LanguageServerProcess.props(
|
||||
progressTracker = bootProgressTracker,
|
||||
descriptor = descriptor,
|
||||
bootTimeout = bootTimeout,
|
||||
rpcPort = rpcPort,
|
||||
dataPort = dataPort,
|
||||
executor = executor
|
||||
),
|
||||
s"process-wrapper-${descriptor.name}"
|
||||
)
|
||||
val server = new LanguageServerComponent(config)
|
||||
server.start().map(_ => config -> server) pipeTo self
|
||||
|
||||
case Failure(th) =>
|
||||
log.error(
|
||||
th,
|
||||
s"An error occurred during boot of Language Server [${descriptor.name}]"
|
||||
case LanguageServerProcess.ServerTerminated(exitCode) =>
|
||||
handleBootFailure(
|
||||
shouldRetry,
|
||||
retryCount,
|
||||
bootRequester,
|
||||
s"Language server terminated with exit code $exitCode before " +
|
||||
s"finishing booting.",
|
||||
None
|
||||
)
|
||||
if (retryCount < config.numberOfRetries) {
|
||||
context.system.scheduler
|
||||
.scheduleOnce(config.delayBetweenRetry, self, FindFreeSocket)
|
||||
context.become(findingSocket(retryCount + 1))
|
||||
} else {
|
||||
|
||||
case LanguageServerProcess.ServerThreadFailed(throwable) =>
|
||||
handleBootFailure(
|
||||
shouldRetry,
|
||||
retryCount,
|
||||
bootRequester,
|
||||
s"Language server thread failed with $throwable.",
|
||||
Some(throwable)
|
||||
)
|
||||
|
||||
case LanguageServerProcess.ServerConfirmedFinishedBooting =>
|
||||
val connectionInfo = LanguageServerConnectionInfo(
|
||||
descriptor.networkConfig.interface,
|
||||
rpcPort = rpcPort,
|
||||
dataPort = dataPort
|
||||
)
|
||||
log.info(s"Language server booted [$connectionInfo].")
|
||||
|
||||
bootRequester ! ServerBooted(connectionInfo, self)
|
||||
context.become(running(connectionInfo))
|
||||
|
||||
case GracefulStop =>
|
||||
context.children.foreach(_ ! LanguageServerProcess.Stop)
|
||||
}
|
||||
|
||||
/** Handles a boot failure by logging it and depending on configuration,
|
||||
* retrying or notifying the proper actor about the failure.
|
||||
*/
|
||||
private def handleBootFailure(
|
||||
shouldRetry: Boolean,
|
||||
retryCount: Int,
|
||||
bootRequester: ActorRef,
|
||||
message: String,
|
||||
throwable: Option[Throwable]
|
||||
): Unit = {
|
||||
log.warning(message)
|
||||
|
||||
if (shouldRetry && retryCount < config.numberOfRetries) {
|
||||
context.system.scheduler
|
||||
.scheduleOnce(config.delayBetweenRetry, self, FindFreeSocket)
|
||||
context.become(findingSocket(retryCount + 1))
|
||||
} else {
|
||||
if (shouldRetry) {
|
||||
log.error(
|
||||
s"Tried $retryCount times to boot Language Server. Giving up."
|
||||
)
|
||||
context.parent ! ServerBootFailed(th)
|
||||
context.stop(self)
|
||||
} else {
|
||||
log.error("Failed to restart the server. Giving up.")
|
||||
}
|
||||
|
||||
case (config: LanguageServerConfig, server: LanguageServerComponent) =>
|
||||
log.info(s"Language server booted [$config].")
|
||||
context.parent ! ServerBooted(config, server)
|
||||
bootRequester ! ServerBootFailed(
|
||||
throwable.getOrElse(new RuntimeException(message))
|
||||
)
|
||||
context.stop(self)
|
||||
}
|
||||
}
|
||||
|
||||
/** After successful boot, we cannot stop as it would stop our child process,
|
||||
* so we just wait for it to terminate.
|
||||
*
|
||||
* The restart command can trigger the restarting phase which consists of two
|
||||
* parts: waiting for the old process to shutdown and rebooting a new one.
|
||||
*/
|
||||
private def running(connectionInfo: LanguageServerConnectionInfo): Receive = {
|
||||
case msg @ LanguageServerProcess.ServerTerminated(exitCode) =>
|
||||
log.debug(
|
||||
s"Language Server process has terminated with exit code $exitCode"
|
||||
)
|
||||
context.parent ! msg
|
||||
context.stop(self)
|
||||
|
||||
case Restart =>
|
||||
context.children.foreach(_ ! LanguageServerProcess.Stop)
|
||||
context.become(
|
||||
restartingWaitingForShutdown(connectionInfo, rebootRequester = sender())
|
||||
)
|
||||
|
||||
case GracefulStop =>
|
||||
context.stop(self)
|
||||
context.children.foreach(_ ! LanguageServerProcess.Stop)
|
||||
}
|
||||
|
||||
// TODO [RW] handling stop timeout (#1315)
|
||||
// may also consider a stop timeout for GracefulStop and killing the process?
|
||||
/** First phase of restart waits fot the old process to shutdown and boots the
|
||||
* new process.
|
||||
*/
|
||||
def restartingWaitingForShutdown(
|
||||
connectionInfo: LanguageServerConnectionInfo,
|
||||
rebootRequester: ActorRef
|
||||
): Receive = {
|
||||
case LanguageServerProcess.ServerTerminated(exitCode) =>
|
||||
log.debug(
|
||||
s"Language Server process has terminated (as requested to reboot) " +
|
||||
s"with exit code $exitCode"
|
||||
)
|
||||
|
||||
context.become(rebooting(connectionInfo, rebootRequester))
|
||||
self ! Boot
|
||||
|
||||
case GracefulStop =>
|
||||
context.children.foreach(_ ! LanguageServerProcess.Stop)
|
||||
}
|
||||
|
||||
/** Currently rebooting does not retry.
|
||||
*
|
||||
* We cannot directly re-use the retry logic from the initial boot, because
|
||||
* we need to keep the ports unchanged, since they are already passed to
|
||||
* other components that will try to connect there.
|
||||
*/
|
||||
def rebooting(
|
||||
connectionInfo: LanguageServerConnectionInfo,
|
||||
rebootRequester: ActorRef
|
||||
): Receive = booting(
|
||||
rpcPort = connectionInfo.rpcPort,
|
||||
dataPort = connectionInfo.dataPort,
|
||||
shouldRetry = false,
|
||||
retryCount = config.numberOfRetries,
|
||||
bootRequester = rebootRequester
|
||||
)
|
||||
|
||||
private def findPort(): Int =
|
||||
Tcp.findAvailablePort(
|
||||
descriptor.networkConfig.interface,
|
||||
@ -107,29 +265,41 @@ class LanguageServerBootLoader(
|
||||
descriptor.networkConfig.maxPort
|
||||
)
|
||||
|
||||
private case object FindFreeSocket
|
||||
private case object Boot
|
||||
}
|
||||
|
||||
object LanguageServerBootLoader {
|
||||
|
||||
/** Creates a configuration object used to create a [[LanguageServerBootLoader]].
|
||||
*
|
||||
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
|
||||
* related to initializing the engine
|
||||
* @param descriptor a LS descriptor
|
||||
* @param config a bootloader config
|
||||
* @param bootTimeout maximum time the server can use to boot,
|
||||
* does not include the time needed to install any missing
|
||||
* components
|
||||
* @param executor an executor service used to start the language server
|
||||
* process
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props(
|
||||
bootProgressTracker: ActorRef,
|
||||
descriptor: LanguageServerDescriptor,
|
||||
config: BootloaderConfig
|
||||
config: BootloaderConfig,
|
||||
bootTimeout: FiniteDuration,
|
||||
executor: LanguageServerExecutor
|
||||
): Props =
|
||||
Props(new LanguageServerBootLoader(descriptor, config))
|
||||
|
||||
/** Find free socket command.
|
||||
*/
|
||||
case object FindFreeSocket
|
||||
|
||||
/** Boot command.
|
||||
*/
|
||||
case object Boot
|
||||
Props(
|
||||
new LanguageServerBootLoader(
|
||||
bootProgressTracker,
|
||||
descriptor,
|
||||
config,
|
||||
bootTimeout,
|
||||
executor: LanguageServerExecutor
|
||||
)
|
||||
)
|
||||
|
||||
/** Signals that server boot failed.
|
||||
*
|
||||
@ -139,12 +309,16 @@ object LanguageServerBootLoader {
|
||||
|
||||
/** Signals that server booted successfully.
|
||||
*
|
||||
* @param config a server config
|
||||
* @param server a server lifecycle component
|
||||
* @param connectionInfo a server config
|
||||
* @param serverProcessManager an actor that manages the server process
|
||||
* lifecycle, currently it is
|
||||
* [[LanguageServerBootLoader]]
|
||||
*/
|
||||
case class ServerBooted(
|
||||
config: LanguageServerConfig,
|
||||
server: LanguageServerComponent
|
||||
connectionInfo: LanguageServerConnectionInfo,
|
||||
serverProcessManager: ActorRef
|
||||
)
|
||||
|
||||
case class ServerTerminated(exitCode: Int)
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
)
|
@ -2,7 +2,6 @@ package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.Status.Failure
|
||||
import akka.actor.{
|
||||
Actor,
|
||||
ActorLogging,
|
||||
@ -14,12 +13,7 @@ import akka.actor.{
|
||||
SupervisorStrategy,
|
||||
Terminated
|
||||
}
|
||||
import akka.pattern.pipe
|
||||
import org.enso.languageserver.boot.LifecycleComponent.ComponentStopped
|
||||
import org.enso.languageserver.boot.{
|
||||
LanguageServerComponent,
|
||||
LanguageServerConfig
|
||||
}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.projectmanager.boot.configuration.{
|
||||
BootloaderConfig,
|
||||
NetworkConfig,
|
||||
@ -34,35 +28,38 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootL
|
||||
ServerBootFailed,
|
||||
ServerBooted
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.{
|
||||
Boot,
|
||||
BootTimeout,
|
||||
ServerDied,
|
||||
ShutDownServer,
|
||||
ShutdownTimeout
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController._
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
|
||||
import org.enso.projectmanager.model.Project
|
||||
import org.enso.projectmanager.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
|
||||
|
||||
/** A language server controller responsible for managing the server lifecycle.
|
||||
* It delegates all tasks to other actors like bootloader or supervisor.
|
||||
*
|
||||
* @param project a project open by the server
|
||||
* @param engineVersion engine version to use for the language server
|
||||
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
|
||||
* related to initializing the engine
|
||||
* @param networkConfig a net config
|
||||
* @param bootloaderConfig a bootloader config
|
||||
* @param supervisionConfig a supervision config
|
||||
* @param timeoutConfig a timeout config
|
||||
* @param distributionConfiguration configuration of the distribution
|
||||
* @param executor an executor service used to start the language server
|
||||
* process
|
||||
*/
|
||||
class LanguageServerController(
|
||||
project: Project,
|
||||
engineVersion: SemVer,
|
||||
bootProgressTracker: ActorRef,
|
||||
networkConfig: NetworkConfig,
|
||||
bootloaderConfig: BootloaderConfig,
|
||||
supervisionConfig: SupervisionConfig,
|
||||
timeoutConfig: TimeoutConfig
|
||||
timeoutConfig: TimeoutConfig,
|
||||
distributionConfiguration: DistributionConfiguration,
|
||||
executor: LanguageServerExecutor
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with Stash
|
||||
@ -72,10 +69,14 @@ class LanguageServerController(
|
||||
|
||||
private val descriptor =
|
||||
LanguageServerDescriptor(
|
||||
name = s"language-server-${project.id}",
|
||||
rootId = UUID.randomUUID(),
|
||||
root = project.path.get,
|
||||
networkConfig = networkConfig
|
||||
name = s"language-server-${project.id}",
|
||||
rootId = UUID.randomUUID(),
|
||||
rootPath = project.path.get,
|
||||
networkConfig = networkConfig,
|
||||
distributionConfiguration = distributionConfiguration,
|
||||
engineVersion = engineVersion,
|
||||
jvmSettings = distributionConfiguration.defaultJVMSettings,
|
||||
discardOutput = distributionConfiguration.shouldDiscardChildOutput
|
||||
)
|
||||
|
||||
override def supervisorStrategy: SupervisorStrategy =
|
||||
@ -92,21 +93,23 @@ class LanguageServerController(
|
||||
case Boot =>
|
||||
val bootloader =
|
||||
context.actorOf(
|
||||
LanguageServerBootLoader.props(descriptor, bootloaderConfig),
|
||||
"bootloader"
|
||||
LanguageServerBootLoader
|
||||
.props(
|
||||
bootProgressTracker,
|
||||
descriptor,
|
||||
bootloaderConfig,
|
||||
timeoutConfig.bootTimeout,
|
||||
executor
|
||||
),
|
||||
s"bootloader-${descriptor.name}"
|
||||
)
|
||||
context.watch(bootloader)
|
||||
val timeoutCancellable =
|
||||
context.system.scheduler.scheduleOnce(30.seconds, self, BootTimeout)
|
||||
context.become(booting(bootloader, timeoutCancellable))
|
||||
context.become(booting(bootloader))
|
||||
|
||||
case _ => stash()
|
||||
}
|
||||
|
||||
private def booting(
|
||||
Bootloader: ActorRef,
|
||||
timeoutCancellable: Cancellable
|
||||
): Receive = {
|
||||
private def booting(Bootloader: ActorRef): Receive = {
|
||||
case BootTimeout =>
|
||||
log.error(s"Booting failed for $descriptor")
|
||||
unstashAll()
|
||||
@ -115,28 +118,25 @@ class LanguageServerController(
|
||||
case ServerBootFailed(th) =>
|
||||
log.error(th, s"Booting failed for $descriptor")
|
||||
unstashAll()
|
||||
timeoutCancellable.cancel()
|
||||
context.become(bootFailed(LanguageServerProtocol.ServerBootFailed(th)))
|
||||
|
||||
case ServerBooted(config, server) =>
|
||||
case ServerBooted(connectionInfo, serverProcessManager) =>
|
||||
unstashAll()
|
||||
timeoutCancellable.cancel()
|
||||
context.become(supervising(config, server))
|
||||
context.become(supervising(connectionInfo, serverProcessManager))
|
||||
context.actorOf(
|
||||
LanguageServerSupervisor.props(
|
||||
config,
|
||||
server,
|
||||
connectionInfo,
|
||||
serverProcessManager,
|
||||
supervisionConfig,
|
||||
new AkkaBasedWebSocketConnectionFactory(),
|
||||
context.system.scheduler
|
||||
),
|
||||
"supervisor"
|
||||
s"supervisor-${descriptor.name}"
|
||||
)
|
||||
|
||||
case Terminated(Bootloader) =>
|
||||
log.error(s"Bootloader for project ${project.name} failed")
|
||||
unstashAll()
|
||||
timeoutCancellable.cancel()
|
||||
context.become(
|
||||
bootFailed(
|
||||
LanguageServerProtocol.ServerBootFailed(
|
||||
@ -149,33 +149,57 @@ class LanguageServerController(
|
||||
}
|
||||
|
||||
private def supervising(
|
||||
config: LanguageServerConfig,
|
||||
server: LanguageServerComponent,
|
||||
connectionInfo: LanguageServerConnectionInfo,
|
||||
serverProcessManager: ActorRef,
|
||||
clients: Set[UUID] = Set.empty
|
||||
): Receive = {
|
||||
case StartServer(clientId, _) =>
|
||||
sender() ! ServerStarted(
|
||||
LanguageServerSockets(
|
||||
Socket(config.interface, config.rpcPort),
|
||||
Socket(config.interface, config.dataPort)
|
||||
case StartServer(clientId, _, requestedEngineVersion, _) =>
|
||||
if (requestedEngineVersion != engineVersion) {
|
||||
sender() ! ServerBootFailed(
|
||||
new IllegalStateException(
|
||||
s"Requested to boot a server version $requestedEngineVersion, " +
|
||||
s"but a server for this project with a different version, " +
|
||||
s"$engineVersion, is already running. Two servers with different " +
|
||||
s"versions cannot be running for a single project."
|
||||
)
|
||||
)
|
||||
)
|
||||
context.become(supervising(config, server, clients + clientId))
|
||||
|
||||
} else {
|
||||
sender() ! ServerStarted(
|
||||
LanguageServerSockets(
|
||||
Socket(connectionInfo.interface, connectionInfo.rpcPort),
|
||||
Socket(connectionInfo.interface, connectionInfo.dataPort)
|
||||
)
|
||||
)
|
||||
context.become(
|
||||
supervising(connectionInfo, serverProcessManager, clients + clientId)
|
||||
)
|
||||
}
|
||||
case Terminated(_) =>
|
||||
log.debug(s"Bootloader for $project terminated.")
|
||||
|
||||
case StopServer(clientId, _) =>
|
||||
removeClient(config, server, clients, clientId, Some(sender()))
|
||||
removeClient(
|
||||
connectionInfo,
|
||||
serverProcessManager,
|
||||
clients,
|
||||
clientId,
|
||||
Some(sender())
|
||||
)
|
||||
|
||||
case ShutDownServer =>
|
||||
shutDownServer(server, None)
|
||||
shutDownServer(None)
|
||||
|
||||
case ClientDisconnected(clientId) =>
|
||||
removeClient(config, server, clients, clientId, None)
|
||||
removeClient(
|
||||
connectionInfo,
|
||||
serverProcessManager,
|
||||
clients,
|
||||
clientId,
|
||||
None
|
||||
)
|
||||
|
||||
case RenameProject(_, oldName, newName) =>
|
||||
val socket = Socket(config.interface, config.rpcPort)
|
||||
val socket = Socket(connectionInfo.interface, connectionInfo.rpcPort)
|
||||
context.actorOf(
|
||||
ProjectRenameAction
|
||||
.props(
|
||||
@ -191,33 +215,31 @@ class LanguageServerController(
|
||||
)
|
||||
|
||||
case ServerDied =>
|
||||
log.error(s"Language server died [$config]")
|
||||
log.error(s"Language server died [$connectionInfo]")
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
private def removeClient(
|
||||
config: LanguageServerConfig,
|
||||
server: LanguageServerComponent,
|
||||
connectionInfo: LanguageServerConnectionInfo,
|
||||
serverProcessManager: ActorRef,
|
||||
clients: Set[UUID],
|
||||
clientId: UUID,
|
||||
maybeRequester: Option[ActorRef]
|
||||
): Unit = {
|
||||
val updatedClients = clients - clientId
|
||||
if (updatedClients.isEmpty) {
|
||||
shutDownServer(server, maybeRequester)
|
||||
shutDownServer(maybeRequester)
|
||||
} else {
|
||||
sender() ! CannotDisconnectOtherClients
|
||||
context.become(supervising(config, server, updatedClients))
|
||||
context.become(
|
||||
supervising(connectionInfo, serverProcessManager, updatedClients)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def shutDownServer(
|
||||
server: LanguageServerComponent,
|
||||
maybeRequester: Option[ActorRef]
|
||||
): Unit = {
|
||||
private def shutDownServer(maybeRequester: Option[ActorRef]): Unit = {
|
||||
log.debug(s"Shutting down a language server for project ${project.id}")
|
||||
context.children.foreach(_ ! GracefulStop)
|
||||
server.stop() pipeTo self
|
||||
val cancellable =
|
||||
context.system.scheduler
|
||||
.scheduleOnce(timeoutConfig.shutdownTimeout, self, ShutdownTimeout)
|
||||
@ -225,7 +247,7 @@ class LanguageServerController(
|
||||
}
|
||||
|
||||
private def bootFailed(failure: ServerStartupFailure): Receive = {
|
||||
case StartServer(_, _) =>
|
||||
case StartServer(_, _, _, _) =>
|
||||
sender() ! failure
|
||||
stop()
|
||||
}
|
||||
@ -234,18 +256,16 @@ class LanguageServerController(
|
||||
cancellable: Cancellable,
|
||||
maybeRequester: Option[ActorRef]
|
||||
): Receive = {
|
||||
case Failure(th) =>
|
||||
case LanguageServerProcess.ServerTerminated(exitCode) =>
|
||||
cancellable.cancel()
|
||||
log.error(
|
||||
th,
|
||||
s"An error occurred during Language server shutdown [$project]."
|
||||
)
|
||||
maybeRequester.foreach(_ ! FailureDuringShutdown(th))
|
||||
stop()
|
||||
|
||||
case ComponentStopped =>
|
||||
cancellable.cancel()
|
||||
log.info(s"Language server shut down successfully [$project].")
|
||||
if (exitCode == 0) {
|
||||
log.info(s"Language server shut down successfully [$project].")
|
||||
} else {
|
||||
log.warning(
|
||||
s"Language server shut down with non-zero exit code: $exitCode " +
|
||||
s"[$project]."
|
||||
)
|
||||
}
|
||||
maybeRequester.foreach(_ ! ServerStopped)
|
||||
stop()
|
||||
|
||||
@ -254,7 +274,7 @@ class LanguageServerController(
|
||||
maybeRequester.foreach(_ ! ServerShutdownTimedOut)
|
||||
stop()
|
||||
|
||||
case StartServer(_, _) =>
|
||||
case StartServer(_, _, _, _) =>
|
||||
sender() ! PreviousInstanceNotShutDown
|
||||
}
|
||||
|
||||
@ -283,26 +303,40 @@ object LanguageServerController {
|
||||
/** Creates a configuration object used to create a [[LanguageServerController]].
|
||||
*
|
||||
* @param project a project open by the server
|
||||
* @param engineVersion engine version to use for the language server
|
||||
* @param bootProgressTracker an [[ActorRef]] that will get progress updates
|
||||
* related to initializing the engine
|
||||
* @param networkConfig a net config
|
||||
* @param bootloaderConfig a bootloader config
|
||||
* @param supervisionConfig a supervision config
|
||||
* @param timeoutConfig a timeout config
|
||||
* @param distributionConfiguration configuration of the distribution
|
||||
* @param executor an executor service used to start the language server
|
||||
* process
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props(
|
||||
project: Project,
|
||||
engineVersion: SemVer,
|
||||
bootProgressTracker: ActorRef,
|
||||
networkConfig: NetworkConfig,
|
||||
bootloaderConfig: BootloaderConfig,
|
||||
supervisionConfig: SupervisionConfig,
|
||||
timeoutConfig: TimeoutConfig
|
||||
timeoutConfig: TimeoutConfig,
|
||||
distributionConfiguration: DistributionConfiguration,
|
||||
executor: LanguageServerExecutor
|
||||
): Props =
|
||||
Props(
|
||||
new LanguageServerController(
|
||||
project,
|
||||
engineVersion,
|
||||
bootProgressTracker,
|
||||
networkConfig,
|
||||
bootloaderConfig,
|
||||
supervisionConfig,
|
||||
timeoutConfig
|
||||
timeoutConfig,
|
||||
distributionConfiguration,
|
||||
executor
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -2,18 +2,32 @@ package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.projectmanager.boot.configuration.NetworkConfig
|
||||
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
|
||||
import org.enso.runtimeversionmanager.runner.JVMSettings
|
||||
|
||||
/** A descriptor used to start up a Language Server.
|
||||
/** A descriptor specifying options related to starting a Language Server.
|
||||
*
|
||||
* @param name a name of the LS
|
||||
* @param rootId a content root id
|
||||
* @param root a path to the content root
|
||||
* @param rootPath a path to the content root
|
||||
* @param networkConfig a network config
|
||||
* @param distributionConfiguration configuration of current distribution, used
|
||||
* to find installed (or install new) engine
|
||||
* versions
|
||||
* @param engineVersion version of the langauge server's engine to use
|
||||
* @param jvmSettings settings to use for the JVM that will host the engine
|
||||
* @param discardOutput specifies if the process output should be discarded or
|
||||
* printed to parent's streams
|
||||
*/
|
||||
case class LanguageServerDescriptor(
|
||||
name: String,
|
||||
rootId: UUID,
|
||||
root: String,
|
||||
networkConfig: NetworkConfig
|
||||
rootPath: String,
|
||||
networkConfig: NetworkConfig,
|
||||
distributionConfiguration: DistributionConfiguration,
|
||||
engineVersion: SemVer,
|
||||
jvmSettings: JVMSettings,
|
||||
discardOutput: Boolean
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.projectmanager.data.LanguageServerSockets
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol.{
|
||||
CheckTimeout,
|
||||
@ -20,13 +22,20 @@ trait LanguageServerGateway[F[+_, +_]] {
|
||||
|
||||
/** Starts a language server.
|
||||
*
|
||||
* It assumes that the required engine version has been preinstalled.
|
||||
*
|
||||
* @param progressTracker an ActorRef that should get notifications when
|
||||
* waiting on a lock
|
||||
* @param clientId a requester id
|
||||
* @param project a project to start
|
||||
* @param version engine version to use for the launched language server
|
||||
* @return either a failure or sockets that a language server listens on
|
||||
*/
|
||||
def start(
|
||||
progressTracker: ActorRef,
|
||||
clientId: UUID,
|
||||
project: Project
|
||||
project: Project,
|
||||
version: SemVer
|
||||
): F[ServerStartupFailure, LanguageServerSockets]
|
||||
|
||||
/** Stops a lang. server.
|
||||
|
@ -5,6 +5,7 @@ import java.util.UUID
|
||||
import akka.actor.{ActorRef, ActorSystem}
|
||||
import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.projectmanager.boot.configuration.TimeoutConfig
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
@ -38,14 +39,23 @@ class LanguageServerGatewayImpl[
|
||||
|
||||
/** @inheritdoc */
|
||||
override def start(
|
||||
progressTracker: ActorRef,
|
||||
clientId: UUID,
|
||||
project: Project
|
||||
project: Project,
|
||||
version: SemVer
|
||||
): F[ServerStartupFailure, LanguageServerSockets] = {
|
||||
implicit val timeout: Timeout = Timeout(timeoutConfig.bootTimeout)
|
||||
implicit val timeout: Timeout = Timeout(2 * timeoutConfig.bootTimeout)
|
||||
|
||||
// TODO [RW] this can timeout if the boot is stuck waiting on a lock, how do
|
||||
// we want to handle that? #1315
|
||||
Async[F]
|
||||
.fromFuture { () =>
|
||||
(registry ? StartServer(clientId, project)).mapTo[ServerStartupResult]
|
||||
(registry ? StartServer(
|
||||
clientId,
|
||||
project,
|
||||
version,
|
||||
progressTracker
|
||||
)).mapTo[ServerStartupResult]
|
||||
}
|
||||
.mapError(_ => ServerBootTimedOut)
|
||||
.flatMap {
|
||||
|
@ -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
|
||||
}
|
@ -2,6 +2,8 @@ package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.projectmanager.data.LanguageServerSockets
|
||||
import org.enso.projectmanager.model.Project
|
||||
|
||||
@ -13,8 +15,16 @@ object LanguageServerProtocol {
|
||||
*
|
||||
* @param clientId the requester id
|
||||
* @param project the project to start
|
||||
* @param engineVersion version of the engine to use
|
||||
* @param progressTracker an actor that should be sent notifications about
|
||||
* locks
|
||||
*/
|
||||
case class StartServer(clientId: UUID, project: Project)
|
||||
case class StartServer(
|
||||
clientId: UUID,
|
||||
project: Project,
|
||||
engineVersion: SemVer,
|
||||
progressTracker: ActorRef
|
||||
)
|
||||
|
||||
/** Base trait for server startup results.
|
||||
*/
|
||||
|
@ -20,21 +20,27 @@ import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProto
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerRegistry.ServerShutDown
|
||||
import org.enso.projectmanager.util.UnhandledLogging
|
||||
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
|
||||
|
||||
/** An actor that routes request regarding lang. server lifecycle to the
|
||||
* right controller that manages the server.
|
||||
* It creates a controller actor, if a server doesn't exists.
|
||||
* It creates a controller actor, if a server doesn't exist.
|
||||
*
|
||||
* @param networkConfig a net config
|
||||
* @param bootloaderConfig a bootloader config
|
||||
* @param supervisionConfig a supervision config
|
||||
* @param timeoutConfig a timeout config
|
||||
* @param distributionConfiguration configuration of the distribution
|
||||
* @param executor an executor service used to start the language server
|
||||
* process
|
||||
*/
|
||||
class LanguageServerRegistry(
|
||||
networkConfig: NetworkConfig,
|
||||
bootloaderConfig: BootloaderConfig,
|
||||
supervisionConfig: SupervisionConfig,
|
||||
timeoutConfig: TimeoutConfig
|
||||
timeoutConfig: TimeoutConfig,
|
||||
distributionConfiguration: DistributionConfiguration,
|
||||
executor: LanguageServerExecutor
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
@ -44,7 +50,7 @@ class LanguageServerRegistry(
|
||||
private def running(
|
||||
serverControllers: Map[UUID, ActorRef] = Map.empty
|
||||
): Receive = {
|
||||
case msg @ StartServer(_, project) =>
|
||||
case msg @ StartServer(_, project, engineVersion, progressTracker) =>
|
||||
if (serverControllers.contains(project.id)) {
|
||||
serverControllers(project.id).forward(msg)
|
||||
} else {
|
||||
@ -52,10 +58,14 @@ class LanguageServerRegistry(
|
||||
LanguageServerController
|
||||
.props(
|
||||
project,
|
||||
engineVersion,
|
||||
progressTracker,
|
||||
networkConfig,
|
||||
bootloaderConfig,
|
||||
supervisionConfig,
|
||||
timeoutConfig
|
||||
timeoutConfig,
|
||||
distributionConfiguration,
|
||||
executor
|
||||
),
|
||||
s"language-server-controller-${project.id}"
|
||||
)
|
||||
@ -116,20 +126,27 @@ object LanguageServerRegistry {
|
||||
* @param bootloaderConfig a bootloader config
|
||||
* @param supervisionConfig a supervision config
|
||||
* @param timeoutConfig a timeout config
|
||||
* @return
|
||||
* @param distributionConfiguration configuration of the distribution
|
||||
* @param executor an executor service used to start the language server
|
||||
* process
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props(
|
||||
networkConfig: NetworkConfig,
|
||||
bootloaderConfig: BootloaderConfig,
|
||||
supervisionConfig: SupervisionConfig,
|
||||
timeoutConfig: TimeoutConfig
|
||||
timeoutConfig: TimeoutConfig,
|
||||
distributionConfiguration: DistributionConfiguration,
|
||||
executor: LanguageServerExecutor
|
||||
): Props =
|
||||
Props(
|
||||
new LanguageServerRegistry(
|
||||
networkConfig,
|
||||
bootloaderConfig,
|
||||
supervisionConfig,
|
||||
timeoutConfig
|
||||
timeoutConfig,
|
||||
distributionConfiguration,
|
||||
executor
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -2,24 +2,24 @@ package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.Status.Failure
|
||||
import akka.actor.{
|
||||
Actor,
|
||||
ActorLogging,
|
||||
ActorRef,
|
||||
Cancellable,
|
||||
Props,
|
||||
Scheduler,
|
||||
Terminated
|
||||
}
|
||||
import akka.pattern.pipe
|
||||
import org.enso.languageserver.boot.LifecycleComponent.ComponentRestarted
|
||||
import org.enso.languageserver.boot.{LanguageServerConfig, LifecycleComponent}
|
||||
import org.enso.projectmanager.boot.configuration.SupervisionConfig
|
||||
import org.enso.projectmanager.data.Socket
|
||||
import org.enso.projectmanager.infrastructure.http.WebSocketConnectionFactory
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.{
|
||||
ServerBootFailed,
|
||||
ServerBooted
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.ServerDied
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerSupervisor.{
|
||||
RestartServer,
|
||||
SendHeartbeat,
|
||||
ServerUnresponsive,
|
||||
StartSupervision
|
||||
@ -30,15 +30,16 @@ import org.enso.projectmanager.util.UnhandledLogging
|
||||
* restarting it when the server is unresponsive. It delegates server
|
||||
* monitoring to the [[HeartbeatSession]] actor.
|
||||
*
|
||||
* @param config a server config
|
||||
* @param server a server handle
|
||||
* @param connectionInfo a server connection info
|
||||
* @param serverProcessManager an actor that manages the lifecycle of the
|
||||
* server process
|
||||
* @param supervisionConfig a supervision config
|
||||
* @param connectionFactory a web socket connection factory
|
||||
* @param scheduler a scheduler
|
||||
*/
|
||||
class LanguageServerSupervisor(
|
||||
config: LanguageServerConfig,
|
||||
server: LifecycleComponent,
|
||||
connectionInfo: LanguageServerConnectionInfo,
|
||||
serverProcessManager: ActorRef,
|
||||
supervisionConfig: SupervisionConfig,
|
||||
connectionFactory: WebSocketConnectionFactory,
|
||||
scheduler: Scheduler
|
||||
@ -69,7 +70,7 @@ class LanguageServerSupervisor(
|
||||
|
||||
private def supervising(cancellable: Cancellable): Receive = {
|
||||
case SendHeartbeat =>
|
||||
val socket = Socket(config.interface, config.rpcPort)
|
||||
val socket = Socket(connectionInfo.interface, connectionInfo.rpcPort)
|
||||
context.actorOf(
|
||||
HeartbeatSession.props(
|
||||
socket,
|
||||
@ -81,40 +82,31 @@ class LanguageServerSupervisor(
|
||||
)
|
||||
|
||||
case ServerUnresponsive =>
|
||||
log.info(s"Server is unresponsive [$config]. Restarting it...")
|
||||
log.info(s"Server is unresponsive [$connectionInfo]. Restarting it...")
|
||||
cancellable.cancel()
|
||||
log.info(s"Restarting first time the server")
|
||||
server.restart() pipeTo self
|
||||
context.become(restarting())
|
||||
log.info(s"Restarting the server")
|
||||
serverProcessManager ! Restart
|
||||
context.become(restarting)
|
||||
|
||||
case GracefulStop =>
|
||||
cancellable.cancel()
|
||||
stop()
|
||||
}
|
||||
|
||||
private def restarting(restartCount: Int = 1): Receive = {
|
||||
case RestartServer =>
|
||||
log.info(s"Restarting $restartCount time the server")
|
||||
server.restart() pipeTo self
|
||||
()
|
||||
private def restarting: Receive = {
|
||||
case ServerBootFailed(_) =>
|
||||
log.error("Cannot restart language server")
|
||||
context.parent ! ServerDied
|
||||
context.stop(self)
|
||||
|
||||
case Failure(th) =>
|
||||
log.error(th, s"An error occurred during restarting the server [$config]")
|
||||
if (restartCount < supervisionConfig.numberOfRestarts) {
|
||||
scheduler.scheduleOnce(
|
||||
supervisionConfig.delayBetweenRestarts,
|
||||
self,
|
||||
RestartServer
|
||||
case ServerBooted(_, newProcessManager) =>
|
||||
if (newProcessManager != serverProcessManager) {
|
||||
log.error(
|
||||
"The process manager actor has changed. This should never happen. " +
|
||||
"Supervisor may no longer work correctly."
|
||||
)
|
||||
context.become(restarting(restartCount + 1))
|
||||
} else {
|
||||
log.error("Cannot restart language server")
|
||||
context.parent ! ServerDied
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
case ComponentRestarted =>
|
||||
log.info(s"Language server restarted [$config]")
|
||||
log.info(s"Language server restarted [$connectionInfo]")
|
||||
val cancellable =
|
||||
scheduler.scheduleAtFixedRate(
|
||||
supervisionConfig.initialDelay,
|
||||
@ -150,8 +142,6 @@ object LanguageServerSupervisor {
|
||||
|
||||
private case object StartSupervision
|
||||
|
||||
private case object RestartServer
|
||||
|
||||
/** A command responsible for initiating heartbeat session.
|
||||
*/
|
||||
case object SendHeartbeat
|
||||
@ -160,26 +150,30 @@ object LanguageServerSupervisor {
|
||||
*/
|
||||
case object ServerUnresponsive
|
||||
|
||||
/** Signals that the heartbeat has been received (only sent if demanded). */
|
||||
case object HeartbeatReceived
|
||||
|
||||
/** Creates a configuration object used to create a [[LanguageServerSupervisor]].
|
||||
*
|
||||
* @param config a server config
|
||||
* @param server a server handle
|
||||
* @param connectionInfo a server config
|
||||
* @param serverProcessManager an actor that manages the lifecycle of the
|
||||
* server process
|
||||
* @param supervisionConfig a supervision config
|
||||
* @param connectionFactory a web socket connection factory
|
||||
* @param scheduler a scheduler
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props(
|
||||
config: LanguageServerConfig,
|
||||
server: LifecycleComponent,
|
||||
connectionInfo: LanguageServerConnectionInfo,
|
||||
serverProcessManager: ActorRef,
|
||||
supervisionConfig: SupervisionConfig,
|
||||
connectionFactory: WebSocketConnectionFactory,
|
||||
scheduler: Scheduler
|
||||
): Props =
|
||||
Props(
|
||||
new LanguageServerSupervisor(
|
||||
config,
|
||||
server,
|
||||
connectionInfo,
|
||||
serverProcessManager,
|
||||
supervisionConfig,
|
||||
connectionFactory,
|
||||
scheduler
|
||||
|
@ -2,8 +2,9 @@ package org.enso.projectmanager.infrastructure
|
||||
|
||||
package object languageserver {
|
||||
|
||||
/** A stop command.
|
||||
*/
|
||||
/** A stop command. */
|
||||
case object GracefulStop
|
||||
|
||||
/** Requests to restart the language server. */
|
||||
case object Restart
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.enso.projectmanager.infrastructure.repository
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.pkg.{Package, PackageManager}
|
||||
@ -73,16 +74,9 @@ class ProjectFileRepository[
|
||||
getAll().map(_.find(_.id == projectId))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def create(
|
||||
override def findPathForNewProject(
|
||||
project: Project
|
||||
): F[ProjectRepositoryFailure, Unit] =
|
||||
for {
|
||||
projectPath <- findTargetPath(project)
|
||||
_ <- createProjectStructure(project, projectPath)
|
||||
_ <- metadataStorage(projectPath)
|
||||
.persist(ProjectMetadata(project))
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
} yield ()
|
||||
): F[ProjectRepositoryFailure, Path] = findTargetPath(project).map(_.toPath)
|
||||
|
||||
private def tryLoadProject(
|
||||
directory: File
|
||||
@ -97,12 +91,13 @@ class ProjectFileRepository[
|
||||
meta <- metaOpt
|
||||
} yield {
|
||||
Project(
|
||||
id = meta.id,
|
||||
name = pkg.name,
|
||||
kind = meta.kind,
|
||||
created = meta.created,
|
||||
lastOpened = meta.lastOpened,
|
||||
path = Some(directory.toString)
|
||||
id = meta.id,
|
||||
name = pkg.name,
|
||||
kind = meta.kind,
|
||||
created = meta.created,
|
||||
engineVersion = pkg.config.ensoVersion,
|
||||
lastOpened = meta.lastOpened,
|
||||
path = Some(directory.toString)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -115,14 +110,6 @@ class ProjectFileRepository[
|
||||
.map(Some(_))
|
||||
.mapError(_.fold(convertFileStorageFailure))
|
||||
|
||||
private def createProjectStructure(
|
||||
project: Project,
|
||||
projectPath: File
|
||||
): F[StorageFailure, Package[File]] =
|
||||
Sync[F]
|
||||
.blockingOp { PackageManager.Default.create(projectPath, project.name) }
|
||||
.mapError(th => StorageFailure(th.toString))
|
||||
|
||||
/** @inheritdoc */
|
||||
override def rename(
|
||||
projectId: UUID,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.enso.projectmanager.infrastructure.repository
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.projectmanager.model.Project
|
||||
@ -18,12 +19,15 @@ trait ProjectRepository[F[+_, +_]] {
|
||||
*/
|
||||
def exists(name: String): F[ProjectRepositoryFailure, Boolean]
|
||||
|
||||
/** Creates the provided user project in the storage.
|
||||
/** Ensures that the path property is set in the project.
|
||||
*
|
||||
* @param project the project to insert
|
||||
* @return
|
||||
* If it was not set, a new path is generated for it. Otherwise, the function
|
||||
* acts as identity.
|
||||
*
|
||||
* @param project the project to find the path for
|
||||
* @return the project, with the updated path
|
||||
*/
|
||||
def create(project: Project): F[ProjectRepositoryFailure, Unit]
|
||||
def findPathForNewProject(project: Project): F[ProjectRepositoryFailure, Path]
|
||||
|
||||
/** Saves the provided user project in the index.
|
||||
*
|
||||
|
@ -3,12 +3,15 @@ package org.enso.projectmanager.model
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.pkg.{DefaultEnsoVersion, EnsoVersion}
|
||||
|
||||
/** Project entity.
|
||||
*
|
||||
* @param id a project id
|
||||
* @param name a project name
|
||||
* @param kind a project kind
|
||||
* @param created a project creation time
|
||||
* @param engineVersion version of the engine associated with the project
|
||||
* @param lastOpened a project last open time
|
||||
* @param path a path to the project structure
|
||||
*/
|
||||
@ -17,6 +20,7 @@ case class Project(
|
||||
name: String,
|
||||
kind: ProjectKind,
|
||||
created: OffsetDateTime,
|
||||
engineVersion: EnsoVersion = DefaultEnsoVersion,
|
||||
lastOpened: Option[OffsetDateTime] = None,
|
||||
path: Option[String] = None
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
|
||||
import org.enso.jsonrpc.{JsonRpcServer, MessageHandler, Method, Request}
|
||||
import org.enso.projectmanager.boot.configuration.TimeoutConfig
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.effect.Exec
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec}
|
||||
import org.enso.projectmanager.event.ClientEvent.{
|
||||
ClientConnected,
|
||||
ClientDisconnected
|
||||
@ -30,7 +30,7 @@ import scala.concurrent.duration._
|
||||
* @param runtimeVersionManagementService version management service
|
||||
* @param timeoutConfig a request timeout config
|
||||
*/
|
||||
class ClientController[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
class ClientController[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
|
||||
clientId: UUID,
|
||||
projectService: ProjectServiceApi[F],
|
||||
globalConfigService: GlobalConfigServiceApi[F],
|
||||
@ -44,11 +44,19 @@ class ClientController[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
private val requestHandlers: Map[Method, Props] =
|
||||
Map(
|
||||
ProjectCreate -> ProjectCreateHandler
|
||||
.props[F](projectService, timeoutConfig.requestTimeout),
|
||||
.props[F](
|
||||
globalConfigService,
|
||||
projectService,
|
||||
timeoutConfig.requestTimeout
|
||||
),
|
||||
ProjectDelete -> ProjectDeleteHandler
|
||||
.props[F](projectService, timeoutConfig.requestTimeout),
|
||||
ProjectOpen -> ProjectOpenHandler
|
||||
.props[F](clientId, projectService, timeoutConfig.bootTimeout),
|
||||
.props[F](
|
||||
clientId,
|
||||
projectService,
|
||||
timeoutConfig.bootTimeout
|
||||
),
|
||||
ProjectClose -> ProjectCloseHandler
|
||||
.props[F](
|
||||
clientId,
|
||||
@ -118,7 +126,7 @@ object ClientController {
|
||||
* @param timeoutConfig a request timeout config
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
def props[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
|
||||
clientId: UUID,
|
||||
projectService: ProjectServiceApi[F],
|
||||
globalConfigService: GlobalConfigServiceApi[F],
|
||||
|
@ -6,7 +6,7 @@ import akka.actor.{ActorRef, ActorSystem}
|
||||
import org.enso.jsonrpc.ClientControllerFactory
|
||||
import org.enso.projectmanager.boot.configuration.TimeoutConfig
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.effect.Exec
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec}
|
||||
import org.enso.projectmanager.service.ProjectServiceApi
|
||||
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementServiceApi
|
||||
@ -19,7 +19,9 @@ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagemen
|
||||
* @param runtimeVersionManagementService version management service
|
||||
* @param timeoutConfig a request timeout config
|
||||
*/
|
||||
class ManagerClientControllerFactory[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
class ManagerClientControllerFactory[
|
||||
F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel
|
||||
](
|
||||
system: ActorSystem,
|
||||
projectService: ProjectServiceApi[F],
|
||||
globalConfigService: GlobalConfigServiceApi[F],
|
||||
|
@ -321,4 +321,8 @@ object ProjectManagementApi {
|
||||
case class GlobalConfigurationAccessError(msg: String)
|
||||
extends Error(4011, msg)
|
||||
|
||||
case class ProjectCreateError(msg: String) extends Error(4012, msg)
|
||||
|
||||
case class LoggingServiceUnavailable(msg: String) extends Error(4013, msg)
|
||||
|
||||
}
|
||||
|
@ -26,12 +26,11 @@ class EngineInstallHandler[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
|
||||
/** @inheritdoc */
|
||||
override def handleRequest = { params =>
|
||||
val progressTracker = sender()
|
||||
for {
|
||||
_ <- service.installEngine(
|
||||
progressTracker,
|
||||
params.version,
|
||||
params.forceInstallBroken.getOrElse(false)
|
||||
progressTracker = self,
|
||||
version = params.version,
|
||||
forceInstallBroken = params.forceInstallBroken.getOrElse(false)
|
||||
)
|
||||
} yield Unused
|
||||
}
|
||||
|
@ -29,9 +29,11 @@ class EngineUninstallHandler[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
|
||||
/** @inheritdoc */
|
||||
override def handleRequest = { params =>
|
||||
val progressTracker = sender()
|
||||
for {
|
||||
_ <- service.uninstallEngine(progressTracker, params.version)
|
||||
_ <- service.uninstallEngine(
|
||||
progressTracker = self,
|
||||
version = params.version
|
||||
)
|
||||
} yield Unused
|
||||
}
|
||||
}
|
||||
|
@ -1,109 +1,86 @@
|
||||
package org.enso.projectmanager.requesthandler
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.pipe
|
||||
import org.enso.jsonrpc.Errors.{NotImplementedError, ServiceError}
|
||||
import org.enso.jsonrpc._
|
||||
import org.enso.pkg.DefaultEnsoVersion
|
||||
import org.enso.projectmanager.control.effect.Exec
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Exec}
|
||||
import org.enso.projectmanager.data.MissingComponentAction
|
||||
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectCreate
|
||||
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.mapFailure
|
||||
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.failureMapper
|
||||
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
|
||||
import org.enso.projectmanager.service.{
|
||||
ProjectServiceApi,
|
||||
ProjectServiceFailure
|
||||
}
|
||||
import org.enso.projectmanager.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/** A request handler for `project/create` commands.
|
||||
*
|
||||
* @param service a project service
|
||||
* @param configurationService the configuration service
|
||||
* @param projectService a project service
|
||||
* @param requestTimeout a request timeout
|
||||
*/
|
||||
class ProjectCreateHandler[F[+_, +_]: Exec](
|
||||
service: ProjectServiceApi[F],
|
||||
class ProjectCreateHandler[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
|
||||
configurationService: GlobalConfigServiceApi[F],
|
||||
projectService: ProjectServiceApi[F],
|
||||
requestTimeout: FiniteDuration
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
override def receive: Receive = requestStage
|
||||
) extends RequestHandler[
|
||||
F,
|
||||
ProjectServiceFailure,
|
||||
ProjectCreate.type,
|
||||
ProjectCreate.Params,
|
||||
ProjectCreate.Result
|
||||
](
|
||||
ProjectCreate,
|
||||
Some(requestTimeout)
|
||||
) {
|
||||
|
||||
import context.dispatcher
|
||||
override def handleRequest = { params =>
|
||||
val version = params.version.getOrElse(DefaultEnsoVersion)
|
||||
val missingComponentAction =
|
||||
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
|
||||
|
||||
private def requestStage: Receive = {
|
||||
case Request(ProjectCreate, id, params: ProjectCreate.Params) =>
|
||||
if (params.version.isDefined) {
|
||||
// TODO [RW] just to indicate that choosing specific version is not yet
|
||||
// implemented, should be removed once that functionality is added
|
||||
sender() ! ResponseError(Some(id), NotImplementedError)
|
||||
context.stop(self)
|
||||
} else {
|
||||
val version = params.version.getOrElse(DefaultEnsoVersion)
|
||||
val missingComponentAction =
|
||||
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
|
||||
Exec[F]
|
||||
.exec(
|
||||
service
|
||||
.createUserProject(params.name, version, missingComponentAction)
|
||||
for {
|
||||
actualVersion <- configurationService
|
||||
.resolveEnsoVersion(version)
|
||||
.mapError { error =>
|
||||
ProjectServiceFailure.ComponentRepositoryAccessFailure(
|
||||
s"Could not determine the default version: $error"
|
||||
)
|
||||
.pipeTo(self)
|
||||
val cancellable =
|
||||
context.system.scheduler
|
||||
.scheduleOnce(requestTimeout, self, RequestTimeout)
|
||||
context.become(responseStage(id, sender(), cancellable))
|
||||
}
|
||||
}
|
||||
|
||||
private def responseStage(
|
||||
id: Id,
|
||||
replyTo: ActorRef,
|
||||
cancellable: Cancellable
|
||||
): Receive = {
|
||||
case Status.Failure(ex) =>
|
||||
log.error(ex, s"Failure during $ProjectCreate operation:")
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
||||
case RequestTimeout =>
|
||||
log.error(s"Request $ProjectCreate with $id timed out")
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
context.stop(self)
|
||||
|
||||
case Left(failure: ProjectServiceFailure) =>
|
||||
log.error(s"Request $id failed due to $failure")
|
||||
replyTo ! ResponseError(Some(id), mapFailure(failure))
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
||||
case Right(projectId: UUID) =>
|
||||
replyTo ! ResponseResult(
|
||||
ProjectCreate,
|
||||
id,
|
||||
ProjectCreate.Result(projectId)
|
||||
}
|
||||
projectId <- projectService.createUserProject(
|
||||
progressTracker = self,
|
||||
name = params.name,
|
||||
engineVersion = actualVersion,
|
||||
missingComponentAction = missingComponentAction
|
||||
)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
} yield ProjectCreate.Result(projectId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ProjectCreateHandler {
|
||||
|
||||
/** Creates a configuration object used to create a [[ProjectCreateHandler]].
|
||||
*
|
||||
* @param service a project service
|
||||
* @param configurationService
|
||||
* @param projectService a project service
|
||||
* @param requestTimeout a request timeout
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props[F[+_, +_]: Exec](
|
||||
service: ProjectServiceApi[F],
|
||||
def props[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel](
|
||||
configurationService: GlobalConfigServiceApi[F],
|
||||
projectService: ProjectServiceApi[F],
|
||||
requestTimeout: FiniteDuration
|
||||
): Props =
|
||||
Props(new ProjectCreateHandler(service, requestTimeout))
|
||||
Props(
|
||||
new ProjectCreateHandler(
|
||||
configurationService,
|
||||
projectService,
|
||||
requestTimeout
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -2,88 +2,59 @@ package org.enso.projectmanager.requesthandler
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Status}
|
||||
import akka.pattern.pipe
|
||||
import org.enso.jsonrpc.Errors.ServiceError
|
||||
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
|
||||
import akka.actor.Props
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.Exec
|
||||
import org.enso.projectmanager.data.{
|
||||
LanguageServerSockets,
|
||||
MissingComponentAction
|
||||
}
|
||||
import org.enso.projectmanager.data.MissingComponentAction
|
||||
import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectOpen
|
||||
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.mapFailure
|
||||
import org.enso.projectmanager.requesthandler.ProjectServiceFailureMapper.failureMapper
|
||||
import org.enso.projectmanager.service.{
|
||||
ProjectServiceApi,
|
||||
ProjectServiceFailure
|
||||
}
|
||||
import org.enso.projectmanager.util.UnhandledLogging
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/** A request handler for `project/open` commands.
|
||||
*
|
||||
* @param clientId the requester id
|
||||
* @param service a project service
|
||||
* @param projectService a project service
|
||||
* @param requestTimeout a request timeout
|
||||
*/
|
||||
class ProjectOpenHandler[F[+_, +_]: Exec](
|
||||
class ProjectOpenHandler[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
clientId: UUID,
|
||||
service: ProjectServiceApi[F],
|
||||
projectService: ProjectServiceApi[F],
|
||||
requestTimeout: FiniteDuration
|
||||
) extends Actor
|
||||
with ActorLogging
|
||||
with UnhandledLogging {
|
||||
override def receive: Receive = requestStage
|
||||
) extends RequestHandler[
|
||||
F,
|
||||
ProjectServiceFailure,
|
||||
ProjectOpen.type,
|
||||
ProjectOpen.Params,
|
||||
ProjectOpen.Result
|
||||
](
|
||||
ProjectOpen,
|
||||
// TODO [RW] maybe we can get rid of this timeout since boot timeout is
|
||||
// handled by the LanguageServerProcess; still the ? message of
|
||||
// LanguageServerGateway will result in timeouts
|
||||
Some(requestTimeout)
|
||||
) {
|
||||
|
||||
import context.dispatcher
|
||||
override def handleRequest = { params =>
|
||||
val missingComponentAction =
|
||||
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
|
||||
|
||||
private def requestStage: Receive = {
|
||||
case Request(ProjectOpen, id, params: ProjectOpen.Params) =>
|
||||
val missingComponentAction =
|
||||
params.missingComponentAction.getOrElse(MissingComponentAction.Fail)
|
||||
Exec[F]
|
||||
.exec(
|
||||
service
|
||||
.openProject(clientId, params.projectId, missingComponentAction)
|
||||
)
|
||||
.pipeTo(self)
|
||||
val cancellable =
|
||||
context.system.scheduler
|
||||
.scheduleOnce(requestTimeout, self, RequestTimeout)
|
||||
context.become(responseStage(id, sender(), cancellable))
|
||||
}
|
||||
|
||||
private def responseStage(
|
||||
id: Id,
|
||||
replyTo: ActorRef,
|
||||
cancellable: Cancellable
|
||||
): Receive = {
|
||||
case Status.Failure(ex) =>
|
||||
log.error(ex, s"Failure during $ProjectOpen operation:")
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
||||
case RequestTimeout =>
|
||||
log.error(s"Request $ProjectOpen with $id timed out")
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
context.stop(self)
|
||||
|
||||
case Left(failure: ProjectServiceFailure) =>
|
||||
log.error(s"Request $id failed due to $failure")
|
||||
replyTo ! ResponseError(Some(id), mapFailure(failure))
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
|
||||
case Right(sockets: LanguageServerSockets) =>
|
||||
replyTo ! ResponseResult(
|
||||
ProjectOpen,
|
||||
id,
|
||||
ProjectOpen.Result(sockets.jsonSocket, sockets.binarySocket)
|
||||
for {
|
||||
sockets <- projectService.openProject(
|
||||
progressTracker = self,
|
||||
clientId = clientId,
|
||||
projectId = params.projectId,
|
||||
missingComponentAction = missingComponentAction
|
||||
)
|
||||
cancellable.cancel()
|
||||
context.stop(self)
|
||||
} yield ProjectOpen.Result(
|
||||
languageServerJsonAddress = sockets.jsonSocket,
|
||||
languageServerBinaryAddress = sockets.binarySocket
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@ -93,15 +64,21 @@ object ProjectOpenHandler {
|
||||
/** Creates a configuration object used to create a [[ProjectOpenHandler]].
|
||||
*
|
||||
* @param clientId the requester id
|
||||
* @param service a project service
|
||||
* @param projectService a project service
|
||||
* @param requestTimeout a request timeout
|
||||
* @return a configuration object
|
||||
*/
|
||||
def props[F[+_, +_]: Exec](
|
||||
def props[F[+_, +_]: Exec: CovariantFlatMap](
|
||||
clientId: UUID,
|
||||
service: ProjectServiceApi[F],
|
||||
projectService: ProjectServiceApi[F],
|
||||
requestTimeout: FiniteDuration
|
||||
): Props =
|
||||
Props(new ProjectOpenHandler(clientId, service, requestTimeout))
|
||||
Props(
|
||||
new ProjectOpenHandler(
|
||||
clientId,
|
||||
projectService,
|
||||
requestTimeout
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ object ProjectServiceFailureMapper {
|
||||
case DataStoreFailure(msg) => ProjectDataStoreError(msg)
|
||||
case ProjectExists => ProjectExistsError
|
||||
case ProjectNotFound => ProjectNotFoundError
|
||||
case ProjectCreateFailed(msg) => ProjectCreateError(msg)
|
||||
case ProjectOpenFailed(msg) => ProjectOpenError(msg)
|
||||
case ProjectCloseFailed(msg) => ProjectCloseError(msg)
|
||||
case ProjectNotOpen => ProjectNotOpenError
|
||||
|
@ -61,7 +61,7 @@ abstract class RequestHandler[
|
||||
.exec(result)
|
||||
.map(_.map(ResponseResult(method, request.id, _)))
|
||||
.pipeTo(self)
|
||||
val cancellable = {
|
||||
val timeoutCancellable = {
|
||||
requestTimeout.map { timeout =>
|
||||
context.system.scheduler.scheduleOnce(
|
||||
timeout,
|
||||
@ -70,7 +70,7 @@ abstract class RequestHandler[
|
||||
)
|
||||
}
|
||||
}
|
||||
context.become(responseStage(request.id, sender(), cancellable))
|
||||
context.become(responseStage(request.id, sender(), timeoutCancellable))
|
||||
}
|
||||
|
||||
/** Defines the actual logic for handling the request.
|
||||
@ -86,12 +86,12 @@ abstract class RequestHandler[
|
||||
private def responseStage(
|
||||
id: Id,
|
||||
replyTo: ActorRef,
|
||||
cancellable: Option[Cancellable]
|
||||
timeoutCancellable: Option[Cancellable]
|
||||
): Receive = {
|
||||
case Status.Failure(ex) =>
|
||||
log.error(ex, s"Failure during $method operation:")
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
cancellable.foreach(_.cancel())
|
||||
timeoutCancellable.foreach(_.cancel())
|
||||
context.stop(self)
|
||||
|
||||
case RequestTimeout =>
|
||||
@ -103,15 +103,34 @@ abstract class RequestHandler[
|
||||
log.error(s"Request $id failed due to $failure")
|
||||
val error = implicitly[FailureMapper[FailureType]].mapFailure(failure)
|
||||
replyTo ! ResponseError(Some(id), error)
|
||||
cancellable.foreach(_.cancel())
|
||||
timeoutCancellable.foreach(_.cancel())
|
||||
context.stop(self)
|
||||
|
||||
case Right(response) =>
|
||||
replyTo ! response
|
||||
cancellable.foreach(_.cancel())
|
||||
timeoutCancellable.foreach(_.cancel())
|
||||
context.stop(self)
|
||||
|
||||
case notification: ProgressNotification =>
|
||||
notification match {
|
||||
case ProgressNotification.TaskStarted(_, _, _) =>
|
||||
abandonTimeout(id, replyTo, timeoutCancellable)
|
||||
case _ =>
|
||||
}
|
||||
replyTo ! translateProgressNotification(method.name, notification)
|
||||
}
|
||||
|
||||
/** Cancels the timeout operation.
|
||||
*
|
||||
* Should be called when a long-running task is detected that we do not want
|
||||
* to interrupt.
|
||||
*/
|
||||
private def abandonTimeout(
|
||||
id: Id,
|
||||
replyTo: ActorRef,
|
||||
timeoutCancellable: Option[Cancellable]
|
||||
): Unit = {
|
||||
timeoutCancellable.foreach(_.cancel())
|
||||
context.become(responseStage(id, replyTo, None))
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
@ -2,19 +2,21 @@ package org.enso.projectmanager.service
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import cats.MonadError
|
||||
import org.enso.pkg.{EnsoVersion, PackageManager}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.pkg.PackageManager
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.core.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.control.effect.syntax._
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.data.{
|
||||
LanguageServerSockets,
|
||||
MissingComponentAction,
|
||||
ProjectMetadata
|
||||
}
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerGateway
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerProtocol._
|
||||
import org.enso.projectmanager.infrastructure.log.Logging
|
||||
import org.enso.projectmanager.infrastructure.random.Generator
|
||||
import org.enso.projectmanager.infrastructure.repository.ProjectRepositoryFailure.{
|
||||
@ -35,11 +37,18 @@ import org.enso.projectmanager.service.ValidationFailure.{
|
||||
EmptyName,
|
||||
NameContainsForbiddenCharacter
|
||||
}
|
||||
import org.enso.projectmanager.service.config.GlobalConfigServiceApi
|
||||
import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
|
||||
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
|
||||
|
||||
/** Implementation of business logic for project management.
|
||||
*
|
||||
* @param validator a project validator
|
||||
* @param repo a project repository
|
||||
* @param projectCreationService a service for creating projects
|
||||
* @param configurationService a service for managing configuration
|
||||
* @param log a logging facility
|
||||
* @param clock a clock
|
||||
* @param gen a random generator
|
||||
@ -47,10 +56,13 @@ import org.enso.projectmanager.service.ValidationFailure.{
|
||||
class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
validator: ProjectValidator[F],
|
||||
repo: ProjectRepository[F],
|
||||
projectCreationService: ProjectCreationServiceApi[F],
|
||||
configurationService: GlobalConfigServiceApi[F],
|
||||
log: Logging[F],
|
||||
clock: Clock[F],
|
||||
gen: Generator[F],
|
||||
languageServerGateway: LanguageServerGateway[F]
|
||||
languageServerGateway: LanguageServerGateway[F],
|
||||
distributionConfiguration: DistributionConfiguration
|
||||
)(implicit E: MonadError[F[ProjectServiceFailure, *], ProjectServiceFailure])
|
||||
extends ProjectServiceApi[F] {
|
||||
|
||||
@ -58,25 +70,30 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
|
||||
/** @inheritdoc */
|
||||
override def createUserProject(
|
||||
progressTracker: ActorRef,
|
||||
name: String,
|
||||
version: EnsoVersion,
|
||||
engineVersion: SemVer,
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, UUID] = {
|
||||
// TODO [RW] new component handling
|
||||
val _ = (version, missingComponentAction)
|
||||
// format: off
|
||||
for {
|
||||
projectId <- gen.randomUUID()
|
||||
_ <- log.debug(s"Creating project $name $projectId.")
|
||||
_ <- validateName(name)
|
||||
_ <- checkIfNameExists(name)
|
||||
creationTime <- clock.nowInUtc()
|
||||
project = Project(projectId, name, UserProject, creationTime)
|
||||
_ <- repo.create(project).mapError(toServiceFailure)
|
||||
_ <- log.info(s"Project $project created.")
|
||||
} yield projectId
|
||||
// format: on
|
||||
}
|
||||
): F[ProjectServiceFailure, UUID] = for {
|
||||
projectId <- gen.randomUUID()
|
||||
_ <- log.debug(s"Creating project $name $projectId.")
|
||||
_ <- validateName(name)
|
||||
_ <- checkIfNameExists(name)
|
||||
creationTime <- clock.nowInUtc()
|
||||
project = Project(projectId, name, UserProject, creationTime)
|
||||
path <- repo.findPathForNewProject(project).mapError(toServiceFailure)
|
||||
_ <- projectCreationService.createProject(
|
||||
progressTracker,
|
||||
path,
|
||||
name,
|
||||
engineVersion,
|
||||
missingComponentAction
|
||||
)
|
||||
_ <- repo
|
||||
.update(project.copy(path = Some(path.toString)))
|
||||
.mapError(toServiceFailure)
|
||||
_ <- log.info(s"Project $project created.")
|
||||
} yield projectId
|
||||
|
||||
/** @inheritdoc */
|
||||
override def deleteUserProject(
|
||||
@ -176,12 +193,11 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
|
||||
/** @inheritdoc */
|
||||
override def openProject(
|
||||
progressTracker: ActorRef,
|
||||
clientId: UUID,
|
||||
projectId: UUID,
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, LanguageServerSockets] = {
|
||||
// TODO [RW] new component handling
|
||||
val _ = missingComponentAction
|
||||
// format: off
|
||||
for {
|
||||
_ <- log.debug(s"Opening project $projectId")
|
||||
@ -189,17 +205,46 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
openTime <- clock.nowInUtc()
|
||||
updated = project.copy(lastOpened = Some(openTime))
|
||||
_ <- repo.update(updated).mapError(toServiceFailure)
|
||||
sockets <- startServer(clientId, updated)
|
||||
sockets <- startServer(progressTracker, clientId, updated, missingComponentAction)
|
||||
} yield sockets
|
||||
// format: on
|
||||
}
|
||||
|
||||
private def preinstallEngine(
|
||||
progressTracker: ActorRef,
|
||||
version: SemVer,
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, Unit] =
|
||||
Sync[F]
|
||||
.blockingOp {
|
||||
RuntimeVersionManagerFactory(distributionConfiguration)
|
||||
.makeRuntimeVersionManager(progressTracker, missingComponentAction)
|
||||
.findOrInstallEngine(version)
|
||||
()
|
||||
}
|
||||
.mapRuntimeManagerErrors(th =>
|
||||
ProjectOpenFailed(
|
||||
s"Cannot install the required engine ${th.getMessage}"
|
||||
)
|
||||
)
|
||||
|
||||
private def startServer(
|
||||
progressTracker: ActorRef,
|
||||
clientId: UUID,
|
||||
project: Project
|
||||
): F[ProjectServiceFailure, LanguageServerSockets] =
|
||||
languageServerGateway
|
||||
.start(clientId, project)
|
||||
project: Project,
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, LanguageServerSockets] = for {
|
||||
version <- configurationService
|
||||
.resolveEnsoVersion(project.engineVersion)
|
||||
.mapError { case ConfigurationFileAccessFailure(message) =>
|
||||
ProjectOpenFailed(
|
||||
s"Could not deduce the default version to use for the project: " +
|
||||
s"$message"
|
||||
)
|
||||
}
|
||||
_ <- preinstallEngine(progressTracker, version, missingComponentAction)
|
||||
sockets <- languageServerGateway
|
||||
.start(progressTracker, clientId, project, version)
|
||||
.mapError {
|
||||
case PreviousInstanceNotShutDown =>
|
||||
ProjectOpenFailed(
|
||||
@ -215,6 +260,7 @@ class ProjectService[F[+_, +_]: ErrorChannel: CovariantFlatMap: Sync](
|
||||
s"Language server boot failed: ${th.getMessage}"
|
||||
)
|
||||
}
|
||||
} yield sockets
|
||||
|
||||
/** @inheritdoc */
|
||||
override def closeProject(
|
||||
|
@ -2,7 +2,8 @@ package org.enso.projectmanager.service
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.pkg.EnsoVersion
|
||||
import akka.actor.ActorRef
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.projectmanager.data.{
|
||||
LanguageServerSockets,
|
||||
MissingComponentAction,
|
||||
@ -17,14 +18,16 @@ trait ProjectServiceApi[F[+_, +_]] {
|
||||
|
||||
/** Creates a user project.
|
||||
*
|
||||
* @param progressTracker the actor to send progress updates to
|
||||
* @param name the name of th project
|
||||
* @param version Enso version to use for the new project
|
||||
* @param engineVersion Enso version to use for the new project
|
||||
* @param missingComponentAction specifies how to handle missing components
|
||||
* @return projectId
|
||||
*/
|
||||
def createUserProject(
|
||||
progressTracker: ActorRef,
|
||||
name: String,
|
||||
version: EnsoVersion,
|
||||
engineVersion: SemVer,
|
||||
missingComponentAction: MissingComponentAction
|
||||
): F[ProjectServiceFailure, UUID]
|
||||
|
||||
@ -48,11 +51,14 @@ trait ProjectServiceApi[F[+_, +_]] {
|
||||
|
||||
/** Opens a project. It starts up a Language Server if needed.
|
||||
*
|
||||
* @param progressTracker the actor to send progress updates to
|
||||
* @param clientId the requester id
|
||||
* @param projectId the project id
|
||||
* @param missingComponentAction specifies how to handle missing components
|
||||
* @return either failure or a socket of the Language Server
|
||||
*/
|
||||
def openProject(
|
||||
progressTracker: ActorRef,
|
||||
clientId: UUID,
|
||||
projectId: UUID,
|
||||
missingComponentAction: MissingComponentAction
|
||||
|
@ -28,6 +28,12 @@ object ProjectServiceFailure {
|
||||
*/
|
||||
case object ProjectNotFound extends ProjectServiceFailure
|
||||
|
||||
/** Signals that a failure occured when creating the project.
|
||||
*
|
||||
* @param message a failure message
|
||||
*/
|
||||
case class ProjectCreateFailed(message: String) extends ProjectServiceFailure
|
||||
|
||||
/** Signals that a failure occurred during project startup.
|
||||
*
|
||||
* @param message a failure message
|
||||
|
@ -1,6 +1,9 @@
|
||||
package org.enso.projectmanager.service.config
|
||||
|
||||
import io.circe.Json
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.pkg.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion}
|
||||
import org.enso.projectmanager.control.core.CovariantFlatMap
|
||||
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
|
||||
import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure
|
||||
import org.enso.projectmanager.service.versionmanagement.NoOpInterface
|
||||
@ -11,7 +14,7 @@ import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
|
||||
*
|
||||
* @param distributionConfiguration a distribution configuration
|
||||
*/
|
||||
class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel](
|
||||
class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel: CovariantFlatMap](
|
||||
distributionConfiguration: DistributionConfiguration
|
||||
) extends GlobalConfigServiceApi[F] {
|
||||
|
||||
@ -20,26 +23,46 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel](
|
||||
distributionConfiguration.distributionManager
|
||||
)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getKey(
|
||||
key: String
|
||||
): F[GlobalConfigServiceFailure, Option[String]] =
|
||||
Sync[F].blockingIO {
|
||||
Sync[F].blockingOp {
|
||||
val valueOption = configurationManager.getConfig.original.apply(key)
|
||||
valueOption.map(json => json.asString.getOrElse(json.toString()))
|
||||
}.recoverAccessErrors
|
||||
|
||||
/** @inheritdoc */
|
||||
override def setKey(
|
||||
key: String,
|
||||
value: String
|
||||
): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingIO {
|
||||
): F[GlobalConfigServiceFailure, Unit] = Sync[F].blockingOp {
|
||||
configurationManager.updateConfigRaw(key, Json.fromString(value))
|
||||
}.recoverAccessErrors
|
||||
|
||||
/** @inheritdoc */
|
||||
override def deleteKey(key: String): F[GlobalConfigServiceFailure, Unit] =
|
||||
Sync[F].blockingIO {
|
||||
Sync[F].blockingOp {
|
||||
configurationManager.removeFromConfig(key)
|
||||
}.recoverAccessErrors
|
||||
|
||||
/** @inheritdoc */
|
||||
override def getDefaultEnsoVersion: F[GlobalConfigServiceFailure, SemVer] =
|
||||
Sync[F].blockingOp {
|
||||
configurationManager.defaultVersion
|
||||
}.recoverAccessErrors
|
||||
|
||||
/** @inheritdoc */
|
||||
override def resolveEnsoVersion(
|
||||
ensoVersion: EnsoVersion
|
||||
): F[GlobalConfigServiceFailure, SemVer] = ensoVersion match {
|
||||
case DefaultEnsoVersion => getDefaultEnsoVersion
|
||||
case SemVerEnsoVersion(version) => CovariantFlatMap[F].pure(version)
|
||||
}
|
||||
|
||||
/** Syntax for recovering arbitrary errors into errors describing
|
||||
* configuration access failure.
|
||||
*/
|
||||
implicit class AccessErrorRecovery[A](fa: F[Throwable, A]) {
|
||||
def recoverAccessErrors: F[GlobalConfigServiceFailure, A] = {
|
||||
ErrorChannel[F].mapError(fa) { throwable =>
|
||||
@ -47,4 +70,5 @@ class GlobalConfigService[F[+_, +_]: Sync: ErrorChannel](
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package org.enso.projectmanager.service.config
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.pkg.EnsoVersion
|
||||
|
||||
/** A contract for the Global Config Service.
|
||||
*
|
||||
* @tparam F a monadic context
|
||||
@ -23,4 +26,19 @@ trait GlobalConfigServiceApi[F[+_, +_]] {
|
||||
* If the value was not present already, nothing happens.
|
||||
*/
|
||||
def deleteKey(key: String): F[GlobalConfigServiceFailure, Unit]
|
||||
|
||||
/** Returns the default engine version.
|
||||
*
|
||||
* It reads the setting from the config, or if no version is set, falls back
|
||||
* to the latest installed (or latest available if none are installed)
|
||||
* version.
|
||||
*/
|
||||
def getDefaultEnsoVersion: F[GlobalConfigServiceFailure, SemVer]
|
||||
|
||||
/** Resolves an [[EnsoVersion]] which can indicate to use a 'default' version
|
||||
* to a concrete version.
|
||||
*/
|
||||
def resolveEnsoVersion(
|
||||
ensoVersion: EnsoVersion
|
||||
): F[GlobalConfigServiceFailure, SemVer]
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import org.enso.projectmanager.service.ProjectServiceFailure.{
|
||||
ComponentRepositoryAccessFailure,
|
||||
ComponentUninstallationFailure
|
||||
}
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
|
||||
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
|
||||
import org.enso.runtimeversionmanager.components.ComponentMissingError
|
||||
|
||||
@ -19,9 +20,10 @@ import org.enso.runtimeversionmanager.components.ComponentMissingError
|
||||
* @param distributionConfiguration a distribution configuration
|
||||
*/
|
||||
class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
|
||||
override val distributionConfiguration: DistributionConfiguration
|
||||
) extends RuntimeVersionManagementServiceApi[F]
|
||||
with RuntimeVersionManagerMixin {
|
||||
distributionConfiguration: DistributionConfiguration
|
||||
) extends RuntimeVersionManagementServiceApi[F] {
|
||||
|
||||
val factory = RuntimeVersionManagerFactory(distributionConfiguration)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def installEngine(
|
||||
@ -31,11 +33,13 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
|
||||
): F[ProjectServiceFailure, Unit] = {
|
||||
Sync[F]
|
||||
.blockingOp {
|
||||
makeRuntimeVersionManager(
|
||||
progressTracker,
|
||||
allowMissingComponents = true,
|
||||
allowBrokenComponents = forceInstallBroken
|
||||
).findOrInstallEngine(version)
|
||||
factory
|
||||
.makeRuntimeVersionManager(
|
||||
progressTracker,
|
||||
allowMissingComponents = true,
|
||||
allowBrokenComponents = forceInstallBroken
|
||||
)
|
||||
.findOrInstallEngine(version)
|
||||
()
|
||||
}
|
||||
.mapRuntimeManagerErrors(throwable =>
|
||||
@ -50,11 +54,13 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
|
||||
): F[ProjectServiceFailure, Unit] = Sync[F]
|
||||
.blockingOp {
|
||||
try {
|
||||
makeRuntimeVersionManager(
|
||||
progressTracker,
|
||||
allowMissingComponents = false,
|
||||
allowBrokenComponents = false
|
||||
).uninstallEngine(version)
|
||||
factory
|
||||
.makeRuntimeVersionManager(
|
||||
progressTracker,
|
||||
allowMissingComponents = false,
|
||||
allowBrokenComponents = false
|
||||
)
|
||||
.uninstallEngine(version)
|
||||
} catch {
|
||||
case _: ComponentMissingError =>
|
||||
}
|
||||
@ -67,7 +73,7 @@ class RuntimeVersionManagementService[F[+_, +_]: Sync: ErrorChannel](
|
||||
override def listInstalledEngines()
|
||||
: F[ProjectServiceFailure, Seq[EngineVersion]] = Sync[F]
|
||||
.blockingOp {
|
||||
makeReadOnlyVersionManager().listInstalledEngines().map {
|
||||
factory.makeReadOnlyVersionManager().listInstalledEngines().map {
|
||||
installedEngine =>
|
||||
EngineVersion(installedEngine.version, installedEngine.isMarkedBroken)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import org.enso.runtimeversionmanager.releases.engine.{
|
||||
EngineRepository
|
||||
}
|
||||
import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider
|
||||
import org.enso.runtimeversionmanager.runner.JVMSettings
|
||||
|
||||
/** Default distribution configuration to use for the Project Manager in
|
||||
* production.
|
||||
@ -27,13 +28,13 @@ import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider
|
||||
object DefaultDistributionConfiguration extends DistributionConfiguration {
|
||||
|
||||
/** The default [[Environment]] implementation, with no overrides. */
|
||||
object DefaultEnvironment extends Environment
|
||||
val environment: Environment = new Environment {}
|
||||
|
||||
// TODO [RW, AO] should the PM support portable distributions?
|
||||
// If so, where will be the project-manager binary located with respect to
|
||||
// the distribution root?
|
||||
/** @inheritdoc */
|
||||
lazy val distributionManager = new DistributionManager(DefaultEnvironment)
|
||||
lazy val distributionManager = new DistributionManager(environment)
|
||||
|
||||
/** @inheritdoc */
|
||||
lazy val lockManager = new FileLockManager(distributionManager.paths.locks)
|
||||
@ -63,4 +64,10 @@ object DefaultDistributionConfiguration extends DistributionConfiguration {
|
||||
engineReleaseProvider = engineReleaseProvider,
|
||||
runtimeReleaseProvider = runtimeReleaseProvider
|
||||
)
|
||||
|
||||
/** @inheritdoc */
|
||||
override def defaultJVMSettings: JVMSettings = JVMSettings.default
|
||||
|
||||
/** @inheritdoc */
|
||||
override def shouldDiscardChildOutput: Boolean = false
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.enso.projectmanager.versionmanagement
|
||||
|
||||
import org.enso.runtimeversionmanager.Environment
|
||||
import org.enso.runtimeversionmanager.components.{
|
||||
RuntimeVersionManagementUserInterface,
|
||||
RuntimeVersionManager
|
||||
@ -11,6 +12,7 @@ import org.enso.runtimeversionmanager.distribution.{
|
||||
import org.enso.runtimeversionmanager.locking.ResourceManager
|
||||
import org.enso.runtimeversionmanager.releases.ReleaseProvider
|
||||
import org.enso.runtimeversionmanager.releases.engine.EngineRelease
|
||||
import org.enso.runtimeversionmanager.runner.JVMSettings
|
||||
|
||||
/** Specifies the configuration of project manager's distribution.
|
||||
*
|
||||
@ -20,6 +22,9 @@ import org.enso.runtimeversionmanager.releases.engine.EngineRelease
|
||||
*/
|
||||
trait DistributionConfiguration {
|
||||
|
||||
/** An [[Environment]] instance. */
|
||||
def environment: Environment
|
||||
|
||||
/** A [[DistributionManager]] instance. */
|
||||
def distributionManager: DistributionManager
|
||||
|
||||
@ -39,4 +44,19 @@ trait DistributionConfiguration {
|
||||
def makeRuntimeVersionManager(
|
||||
userInterface: RuntimeVersionManagementUserInterface
|
||||
): RuntimeVersionManager
|
||||
|
||||
/** Default set of JVM settings to use when launching the runner.
|
||||
*
|
||||
* This is exposed mostly for ease of overriding the settings in tests.
|
||||
*/
|
||||
def defaultJVMSettings: JVMSettings
|
||||
|
||||
/** Specifies if output of the child Language Server process should be ignored
|
||||
* or piped to parent's streams.
|
||||
*
|
||||
* This option is used to easily turn off logging in tests.
|
||||
*
|
||||
* TODO [RW] It will likely become obsolete once #1151 (or #1144) is done.
|
||||
*/
|
||||
def shouldDiscardChildOutput: Boolean
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ project-manager {
|
||||
io-timeout = 5 seconds
|
||||
request-timeout = 10 seconds
|
||||
boot-timeout = 30 seconds
|
||||
shutdown-timeout = 10 seconds
|
||||
shutdown-timeout = 20 seconds
|
||||
socket-close-timeout = 2 seconds
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -1,19 +1,26 @@
|
||||
package org.enso.projectmanager
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.time.{OffsetDateTime, ZoneOffset}
|
||||
import java.util.UUID
|
||||
|
||||
import akka.testkit.TestActors.blackholeProps
|
||||
import akka.testkit._
|
||||
import io.circe.Json
|
||||
import io.circe.parser.parse
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.jsonrpc.test.JsonRpcServerTestKit
|
||||
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
|
||||
import org.enso.loggingservice.printers.StderrPrinterWithColors
|
||||
import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager}
|
||||
import org.enso.projectmanager.boot.Globals.{ConfigFilename, ConfigNamespace}
|
||||
import org.enso.projectmanager.boot.configuration._
|
||||
import org.enso.projectmanager.control.effect.ZioEnvExec
|
||||
import org.enso.projectmanager.infrastructure.file.BlockingFileSystem
|
||||
import org.enso.projectmanager.infrastructure.languageserver.{
|
||||
ExecutorWithUnlimitedPool,
|
||||
LanguageServerGatewayImpl,
|
||||
LanguageServerRegistry,
|
||||
ShutdownHookActivator
|
||||
@ -26,18 +33,27 @@ import org.enso.projectmanager.protocol.{
|
||||
}
|
||||
import org.enso.projectmanager.service.config.GlobalConfigService
|
||||
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagementService
|
||||
import org.enso.projectmanager.service.{MonadicProjectValidator, ProjectService}
|
||||
import org.enso.projectmanager.service.{
|
||||
MonadicProjectValidator,
|
||||
ProjectCreationService,
|
||||
ProjectService
|
||||
}
|
||||
import org.enso.projectmanager.test.{ObservableGenerator, ProgrammableClock}
|
||||
import org.enso.runtimeversionmanager.OS
|
||||
import org.enso.runtimeversionmanager.test.{DropLogs, FakeReleases}
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import pureconfig.ConfigSource
|
||||
import pureconfig.generic.auto._
|
||||
import zio.interop.catz.core._
|
||||
import zio.{Runtime, Semaphore, ZEnv, ZIO}
|
||||
|
||||
import scala.concurrent.{Await, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
|
||||
class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
|
||||
class BaseServerSpec
|
||||
extends JsonRpcServerTestKit
|
||||
with DropLogs
|
||||
with BeforeAndAfterAll {
|
||||
|
||||
override def protocol: Protocol = JsonRpc.protocol
|
||||
|
||||
@ -97,10 +113,25 @@ class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
|
||||
|
||||
lazy val projectValidator = new MonadicProjectValidator[ZIO[ZEnv, *, *]]()
|
||||
|
||||
lazy val distributionConfiguration =
|
||||
TestDistributionConfiguration(
|
||||
distributionRoot = testDistributionRoot.toPath,
|
||||
engineReleaseProvider = FakeReleases.engineReleaseProvider,
|
||||
runtimeReleaseProvider = FakeReleases.runtimeReleaseProvider,
|
||||
discardChildOutput = !debugChildLogs
|
||||
)
|
||||
|
||||
lazy val languageServerRegistry =
|
||||
system.actorOf(
|
||||
LanguageServerRegistry
|
||||
.props(netConfig, bootloaderConfig, supervisionConfig, timeoutConfig)
|
||||
.props(
|
||||
netConfig,
|
||||
bootloaderConfig,
|
||||
supervisionConfig,
|
||||
timeoutConfig,
|
||||
distributionConfiguration,
|
||||
ExecutorWithUnlimitedPool
|
||||
)
|
||||
)
|
||||
|
||||
lazy val shutdownHookActivator =
|
||||
@ -114,27 +145,26 @@ class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
|
||||
timeoutConfig
|
||||
)
|
||||
|
||||
lazy val projectService =
|
||||
new ProjectService[ZIO[ZEnv, +*, +*]](
|
||||
projectValidator,
|
||||
projectRepository,
|
||||
new Slf4jLogging[ZIO[ZEnv, +*, +*]],
|
||||
testClock,
|
||||
gen,
|
||||
languageServerGateway
|
||||
)
|
||||
|
||||
lazy val distributionConfiguration =
|
||||
TestDistributionConfiguration(
|
||||
distributionRoot = testDistributionRoot.toPath,
|
||||
engineReleaseProvider = FakeReleases.engineReleaseProvider,
|
||||
runtimeReleaseProvider = FakeReleases.runtimeReleaseProvider
|
||||
)
|
||||
lazy val projectCreationService =
|
||||
new ProjectCreationService[ZIO[ZEnv, +*, +*]](distributionConfiguration)
|
||||
|
||||
lazy val globalConfigService = new GlobalConfigService[ZIO[ZEnv, +*, +*]](
|
||||
distributionConfiguration
|
||||
)
|
||||
|
||||
lazy val projectService =
|
||||
new ProjectService[ZIO[ZEnv, +*, +*]](
|
||||
projectValidator,
|
||||
projectRepository,
|
||||
projectCreationService,
|
||||
globalConfigService,
|
||||
new Slf4jLogging[ZIO[ZEnv, +*, +*]],
|
||||
testClock,
|
||||
gen,
|
||||
languageServerGateway,
|
||||
distributionConfiguration
|
||||
)
|
||||
|
||||
lazy val runtimeVersionManagementService =
|
||||
new RuntimeVersionManagementService[ZIO[ZEnv, +*, +*]](
|
||||
distributionConfiguration
|
||||
@ -150,9 +180,141 @@ class BaseServerSpec extends JsonRpcServerTestKit with DropLogs {
|
||||
)
|
||||
}
|
||||
|
||||
/** Can be used to avoid deleting the project's root. */
|
||||
val deleteProjectsRootAfterEachTest = true
|
||||
|
||||
override def afterEach(): Unit = {
|
||||
super.afterEach()
|
||||
|
||||
if (deleteProjectsRootAfterEachTest)
|
||||
FileUtils.deleteQuietly(testProjectsRoot)
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
super.afterAll()
|
||||
|
||||
FileUtils.deleteQuietly(testProjectsRoot)
|
||||
}
|
||||
|
||||
/** Tests can override this value to request a specific engine version to be
|
||||
* preinstalled when running the suite.
|
||||
*/
|
||||
val engineToInstall: Option[SemVer] = None
|
||||
|
||||
/** Tests can override this to set up a logging service that will print debug
|
||||
* logs.
|
||||
*/
|
||||
val debugLogs: Boolean = false
|
||||
|
||||
/** Tests can override this to allow child process output to be displayed. */
|
||||
val debugChildLogs: Boolean = false
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
super.beforeAll()
|
||||
|
||||
if (debugLogs) {
|
||||
LoggingServiceManager.setup(
|
||||
LoggerMode.Local(
|
||||
Seq(StderrPrinterWithColors.colorPrinterIfAvailable(true))
|
||||
),
|
||||
LogLevel.Trace
|
||||
)
|
||||
}
|
||||
|
||||
engineToInstall.foreach(preInstallEngine)
|
||||
}
|
||||
|
||||
/** This is a temporary solution to ensure that a valid engine distribution is
|
||||
* preinstalled.
|
||||
*
|
||||
* In the future the fake release mechanism can be properly updated to allow
|
||||
* for this kind of configuration without special logic.
|
||||
*/
|
||||
def preInstallEngine(version: SemVer): Unit = {
|
||||
val os = OS.operatingSystem.configName
|
||||
val ext = if (OS.isWindows) "zip" else "tar.gz"
|
||||
val arch = OS.architecture
|
||||
val path = FakeReleases.releaseRoot
|
||||
.resolve("enso")
|
||||
.resolve(s"enso-$version")
|
||||
.resolve(s"enso-engine-$version-$os-$arch.$ext")
|
||||
.resolve(s"enso-$version")
|
||||
.resolve("component")
|
||||
val root = Path.of("../../../").toAbsolutePath.normalize
|
||||
FileUtils.copyFile(
|
||||
root.resolve("runner.jar").toFile,
|
||||
path.resolve("runner.jar").toFile
|
||||
)
|
||||
FileUtils.copyFile(
|
||||
root.resolve("runtime.jar").toFile,
|
||||
path.resolve("runtime.jar").toFile
|
||||
)
|
||||
|
||||
val blackhole = system.actorOf(blackholeProps)
|
||||
val installAction = runtimeVersionManagementService.installEngine(
|
||||
blackhole,
|
||||
version,
|
||||
forceInstallBroken = false
|
||||
)
|
||||
Runtime.default.unsafeRun(installAction)
|
||||
}
|
||||
|
||||
def uninstallEngine(version: SemVer): Unit = {
|
||||
val blackhole = system.actorOf(blackholeProps)
|
||||
val action = runtimeVersionManagementService.uninstallEngine(
|
||||
blackhole,
|
||||
version
|
||||
)
|
||||
Runtime.default.unsafeRun(action)
|
||||
}
|
||||
|
||||
implicit class ClientSyntax(client: WsTestClient) {
|
||||
def expectTaskStarted(
|
||||
timeout: FiniteDuration = 20.seconds.dilated
|
||||
): Unit = {
|
||||
inside(parse(client.expectMessage(timeout))) { case Right(json) =>
|
||||
getMethod(json) shouldEqual Some("task/started")
|
||||
}
|
||||
}
|
||||
|
||||
private def getMethod(json: Json): Option[String] = for {
|
||||
obj <- json.asObject
|
||||
method <- obj("method").flatMap(_.asString)
|
||||
} yield method
|
||||
|
||||
def expectJsonIgnoring(
|
||||
shouldIgnore: Json => Boolean,
|
||||
timeout: FiniteDuration = 20.seconds.dilated
|
||||
): Json = {
|
||||
inside(parse(client.expectMessage(timeout))) { case Right(json) =>
|
||||
if (shouldIgnore(json)) expectJsonIgnoring(shouldIgnore, timeout)
|
||||
else json
|
||||
}
|
||||
}
|
||||
|
||||
def expectError(
|
||||
expectedCode: Int,
|
||||
timeout: FiniteDuration = 10.seconds.dilated
|
||||
): Unit = {
|
||||
withClue("Response should be an error: ") {
|
||||
inside(parse(client.expectMessage(timeout))) { case Right(json) =>
|
||||
val code = for {
|
||||
obj <- json.asObject
|
||||
error <- obj("error").flatMap(_.asObject)
|
||||
code <- error("code").flatMap(_.asNumber).flatMap(_.toInt)
|
||||
} yield code
|
||||
code shouldEqual Some(expectedCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def expectJsonAfterSomeProgress(
|
||||
json: Json,
|
||||
timeout: FiniteDuration = 10.seconds.dilated
|
||||
): Unit =
|
||||
expectJsonIgnoring(
|
||||
json => getMethod(json).exists(_.startsWith("task/")),
|
||||
timeout
|
||||
) shouldEqual json
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.enso.projectmanager
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.testkit.TestDuration
|
||||
import io.circe.Json
|
||||
import io.circe.syntax._
|
||||
import io.circe.literal._
|
||||
@ -58,7 +59,7 @@ trait ProjectManagementOps { this: BaseServerSpec =>
|
||||
}
|
||||
}
|
||||
""")
|
||||
val Right(openReply) = parse(client.expectMessage(10.seconds))
|
||||
val Right(openReply) = parse(client.expectMessage(10.seconds.dilated))
|
||||
val socket = for {
|
||||
result <- openReply.hcursor.downExpectedField("result")
|
||||
addr <- result.downExpectedField("languageServerJsonAddress")
|
||||
@ -81,13 +82,16 @@ trait ProjectManagementOps { this: BaseServerSpec =>
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
client.expectJson(
|
||||
json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"id":0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
""",
|
||||
10.seconds.dilated
|
||||
)
|
||||
}
|
||||
|
||||
def deleteProject(
|
||||
|
@ -25,12 +25,14 @@ import org.enso.runtimeversionmanager.releases.{
|
||||
ReleaseProvider,
|
||||
SimpleReleaseProvider
|
||||
}
|
||||
import org.enso.runtimeversionmanager.runner.{JVMSettings, JavaCommand}
|
||||
import org.enso.runtimeversionmanager.test.{
|
||||
FakeEnvironment,
|
||||
HasTestDirectory,
|
||||
TestLocalLockManager
|
||||
}
|
||||
|
||||
import scala.jdk.OptionConverters.RichOptional
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/** A distribution configuration for use in tests.
|
||||
@ -39,20 +41,23 @@ import scala.util.{Failure, Success, Try}
|
||||
* within some temporary directory
|
||||
* @param engineReleaseProvider provider of (fake) engine releases
|
||||
* @param runtimeReleaseProvider provider of (fake) Graal releases
|
||||
* @param discardChildOutput specifies if input of launched runner processes
|
||||
* should be ignored
|
||||
*/
|
||||
class TestDistributionConfiguration(
|
||||
distributionRoot: Path,
|
||||
override val engineReleaseProvider: ReleaseProvider[EngineRelease],
|
||||
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider
|
||||
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider,
|
||||
discardChildOutput: Boolean
|
||||
) extends DistributionConfiguration
|
||||
with FakeEnvironment
|
||||
with HasTestDirectory {
|
||||
|
||||
def getTestDirectory: Path = distributionRoot
|
||||
|
||||
lazy val distributionManager = new DistributionManager(
|
||||
fakeInstalledEnvironment()
|
||||
)
|
||||
lazy val environment = fakeInstalledEnvironment()
|
||||
|
||||
lazy val distributionManager = new DistributionManager(environment)
|
||||
|
||||
lazy val lockManager = new TestLocalLockManager
|
||||
|
||||
@ -71,36 +76,66 @@ class TestDistributionConfiguration(
|
||||
engineReleaseProvider = engineReleaseProvider,
|
||||
runtimeReleaseProvider = runtimeReleaseProvider
|
||||
)
|
||||
}
|
||||
|
||||
object TestDistributionConfiguration {
|
||||
def withoutReleases(distributionRoot: Path): TestDistributionConfiguration = {
|
||||
val noReleaseProvider = new SimpleReleaseProvider {
|
||||
override def releaseForTag(tag: String): Try[Release] = Failure(
|
||||
new IllegalStateException(
|
||||
"This provider does not support fetching releases."
|
||||
)
|
||||
)
|
||||
|
||||
override def listReleases(): Try[Seq[Release]] = Success(Seq())
|
||||
}
|
||||
|
||||
new TestDistributionConfiguration(
|
||||
distributionRoot = distributionRoot,
|
||||
engineReleaseProvider = new EngineReleaseProvider(noReleaseProvider),
|
||||
runtimeReleaseProvider = new GraalCEReleaseProvider(noReleaseProvider)
|
||||
/** JVM settings that will force to use the same JVM that we are running.
|
||||
*
|
||||
* This is done to avoiding downloading GraalVM in tests (that would be far
|
||||
* too slow) and to ensure that a GraalVM instance is selected, regardless of
|
||||
* the default JVM set in the current environment.
|
||||
*/
|
||||
override def defaultJVMSettings: JVMSettings = {
|
||||
val currentProcess =
|
||||
ProcessHandle.current().info().command().toScala.getOrElse("java")
|
||||
val javaCommand = JavaCommand(currentProcess, None)
|
||||
new JVMSettings(
|
||||
javaCommandOverride = Some(javaCommand),
|
||||
jvmOptions = Seq()
|
||||
)
|
||||
}
|
||||
|
||||
override def shouldDiscardChildOutput: Boolean = discardChildOutput
|
||||
}
|
||||
|
||||
object TestDistributionConfiguration {
|
||||
|
||||
/** Creates a [[TestDistributionConfiguration]] with repositories that do not
|
||||
* have any available releases.
|
||||
*/
|
||||
def withoutReleases(
|
||||
distributionRoot: Path,
|
||||
discardChildOutput: Boolean
|
||||
): TestDistributionConfiguration = {
|
||||
val noReleaseProvider = new NoReleaseProvider
|
||||
new TestDistributionConfiguration(
|
||||
distributionRoot = distributionRoot,
|
||||
engineReleaseProvider = new EngineReleaseProvider(noReleaseProvider),
|
||||
runtimeReleaseProvider = new GraalCEReleaseProvider(noReleaseProvider),
|
||||
discardChildOutput
|
||||
)
|
||||
}
|
||||
|
||||
/** Creates a [[TestDistributionConfiguration]] instance. */
|
||||
def apply(
|
||||
distributionRoot: Path,
|
||||
engineReleaseProvider: ReleaseProvider[EngineRelease],
|
||||
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider
|
||||
runtimeReleaseProvider: GraalVMRuntimeReleaseProvider,
|
||||
discardChildOutput: Boolean
|
||||
): TestDistributionConfiguration =
|
||||
new TestDistributionConfiguration(
|
||||
distributionRoot,
|
||||
engineReleaseProvider,
|
||||
runtimeReleaseProvider
|
||||
runtimeReleaseProvider,
|
||||
discardChildOutput
|
||||
)
|
||||
|
||||
/** A [[SimpleReleaseProvider]] that has no releases. */
|
||||
private class NoReleaseProvider extends SimpleReleaseProvider {
|
||||
override def releaseForTag(tag: String): Try[Release] = Failure(
|
||||
new IllegalStateException(
|
||||
"This provider does not support fetching releases."
|
||||
)
|
||||
)
|
||||
|
||||
override def listReleases(): Try[Seq[Release]] = Success(Seq())
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import akka.testkit.TestDuration
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.projectmanager.test.Net._
|
||||
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
|
||||
import org.enso.testkit.FlakySpec
|
||||
import org.enso.testkit.{FlakySpec, RetrySpec}
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
@ -10,11 +12,14 @@ import scala.concurrent.duration._
|
||||
class LanguageServerGatewaySpec
|
||||
extends BaseServerSpec
|
||||
with FlakySpec
|
||||
with ProjectManagementOps {
|
||||
with ProjectManagementOps
|
||||
with RetrySpec {
|
||||
|
||||
override val engineToInstall = Some(SemVer(0, 0, 1))
|
||||
|
||||
"A language server service" must {
|
||||
|
||||
"kill all running language servers" ignore {
|
||||
"kill all running language servers" taggedAs Retry ignore {
|
||||
implicit val client = new WsTestClient(address)
|
||||
val fooId = createProject("foo")
|
||||
val barId = createProject("bar")
|
||||
@ -27,7 +32,7 @@ class LanguageServerGatewaySpec
|
||||
tryConnect(bazSocket).isRight shouldBe true
|
||||
//when
|
||||
val future = exec.exec(languageServerGateway.killAllServers())
|
||||
Await.result(future, 20.seconds)
|
||||
Await.result(future, 30.seconds.dilated)
|
||||
//then
|
||||
tryConnect(fooSocket).isLeft shouldBe true
|
||||
tryConnect(barSocket).isLeft shouldBe true
|
||||
|
@ -1,15 +1,11 @@
|
||||
package org.enso.projectmanager.infrastructure.languageserver
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import akka.actor.{ActorSystem, Props}
|
||||
import akka.testkit.{ImplicitSender, TestKit, TestProbe}
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.testkit.{ImplicitSender, TestActor, TestKit, TestProbe}
|
||||
import com.miguno.akka.testing.VirtualTime
|
||||
import org.enso.languageserver.boot.LifecycleComponent.ComponentRestarted
|
||||
import org.enso.languageserver.boot.{LanguageServerConfig, LifecycleComponent}
|
||||
import org.enso.projectmanager.boot.configuration.SupervisionConfig
|
||||
import org.enso.projectmanager.infrastructure.http.AkkaBasedWebSocketConnectionFactory
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerController.ServerDied
|
||||
import org.enso.projectmanager.infrastructure.languageserver.LanguageServerBootLoader.ServerBooted
|
||||
import org.enso.projectmanager.infrastructure.languageserver.ProgrammableWebSocketServer.{
|
||||
Reject,
|
||||
ReplyWith
|
||||
@ -17,14 +13,11 @@ import org.enso.projectmanager.infrastructure.languageserver.ProgrammableWebSock
|
||||
import org.enso.projectmanager.infrastructure.languageserver.StepParent.ChildTerminated
|
||||
import org.enso.projectmanager.infrastructure.net.Tcp
|
||||
import org.enso.testkit.FlakySpec
|
||||
import org.mockito.BDDMockito._
|
||||
import org.mockito.Mockito._
|
||||
import org.mockito.MockitoSugar
|
||||
import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.flatspec.AnyFlatSpecLike
|
||||
import org.scalatest.matchers.must.Matchers
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class LanguageServerSupervisorSpec
|
||||
@ -55,7 +48,7 @@ class LanguageServerSupervisorSpec
|
||||
virtualTimeAdvances(testHeartbeatInterval / 2)
|
||||
}
|
||||
//then
|
||||
`then`(serverComponent.restart()).shouldHaveNoInteractions()
|
||||
processManagerProbe.expectNoMessage()
|
||||
//teardown
|
||||
parent ! GracefulStop
|
||||
parentProbe.expectMsg(ChildTerminated)
|
||||
@ -65,8 +58,6 @@ class LanguageServerSupervisorSpec
|
||||
|
||||
it should "restart server when pong message doesn't arrive on time" taggedAs Flaky in new TestCtx {
|
||||
//given
|
||||
when(serverComponent.restart())
|
||||
.thenReturn(Future.successful(ComponentRestarted))
|
||||
val probe = TestProbe()
|
||||
@volatile var pingCount = 0
|
||||
fakeServer.withBehaviour { case ping @ PingMatcher(requestId) =>
|
||||
@ -84,7 +75,7 @@ class LanguageServerSupervisorSpec
|
||||
//when
|
||||
virtualTimeAdvances(testInitialDelay)
|
||||
(1 to 2).foreach { _ =>
|
||||
verifyNoInteractions(serverComponent)
|
||||
processManagerProbe.expectNoMessage()
|
||||
probe.expectMsgPF() { case PingMatcher(_) => () }
|
||||
virtualTimeAdvances(testHeartbeatInterval / 2)
|
||||
probe.expectNoMessage()
|
||||
@ -92,10 +83,11 @@ class LanguageServerSupervisorSpec
|
||||
}
|
||||
probe.expectMsgPF() { case PingMatcher(_) => () }
|
||||
virtualTimeAdvances(testHeartbeatTimeout)
|
||||
verify(serverComponent, timeout(VerificationTimeout).times(1)).restart()
|
||||
processManagerProbe.expectMsg(Restart)
|
||||
restartRequests mustEqual 1
|
||||
virtualTimeAdvances(testInitialDelay)
|
||||
(1 to 2).foreach { _ =>
|
||||
verifyNoMoreInteractions(serverComponent)
|
||||
processManagerProbe.expectNoMessage()
|
||||
probe.expectMsgPF() { case PingMatcher(_) => () }
|
||||
virtualTimeAdvances(testHeartbeatInterval / 2)
|
||||
probe.expectNoMessage()
|
||||
@ -108,35 +100,6 @@ class LanguageServerSupervisorSpec
|
||||
fakeServer.stop()
|
||||
}
|
||||
|
||||
it should "restart server limited number of times" in new TestCtx {
|
||||
//given
|
||||
when(serverComponent.restart()).thenReturn(Future.failed(new Exception))
|
||||
val probe = TestProbe()
|
||||
fakeServer.withBehaviour { case ping @ PingMatcher(_) =>
|
||||
probe.ref ! ping
|
||||
Reject
|
||||
}
|
||||
probe.expectNoMessage()
|
||||
//when
|
||||
virtualTimeAdvances(testInitialDelay)
|
||||
probe.expectMsgPF(5.seconds) { case PingMatcher(_) => () }
|
||||
verifyNoInteractions(serverComponent)
|
||||
virtualTimeAdvances(testHeartbeatTimeout)
|
||||
(1 to testRestartLimit).foreach { i =>
|
||||
verify(serverComponent, timeout(VerificationTimeout).times(i)).restart()
|
||||
virtualTimeAdvances(testRestartDelay)
|
||||
}
|
||||
virtualTimeAdvances(testHeartbeatInterval)
|
||||
probe.expectNoMessage()
|
||||
verifyNoMoreInteractions(serverComponent)
|
||||
//then
|
||||
parentProbe.expectMsg(ServerDied)
|
||||
parentProbe.expectMsg(ChildTerminated)
|
||||
//teardown
|
||||
system.stop(parent)
|
||||
fakeServer.stop()
|
||||
}
|
||||
|
||||
override def afterAll(): Unit = {
|
||||
TestKit.shutdownActorSystem(system)
|
||||
}
|
||||
@ -147,8 +110,6 @@ class LanguageServerSupervisorSpec
|
||||
|
||||
val virtualTime = new VirtualTime
|
||||
|
||||
val serverComponent = mock[LifecycleComponent]
|
||||
|
||||
val testHost = "127.0.0.1"
|
||||
|
||||
val testRpcPort = Tcp.findAvailablePort(testHost, 49152, 55535)
|
||||
@ -168,13 +129,11 @@ class LanguageServerSupervisorSpec
|
||||
val fakeServer = new ProgrammableWebSocketServer(testHost, testRpcPort)
|
||||
fakeServer.start()
|
||||
|
||||
val serverConfig =
|
||||
LanguageServerConfig(
|
||||
val connectionInfo =
|
||||
LanguageServerConnectionInfo(
|
||||
testHost,
|
||||
testRpcPort,
|
||||
testDataPort,
|
||||
UUID.randomUUID(),
|
||||
"/tmp"
|
||||
testDataPort
|
||||
)
|
||||
|
||||
val supervisionConfig =
|
||||
@ -188,15 +147,28 @@ class LanguageServerSupervisorSpec
|
||||
|
||||
val parentProbe = TestProbe()
|
||||
|
||||
val processManagerProbe = TestProbe()
|
||||
var restartRequests = 0
|
||||
processManagerProbe
|
||||
.setAutoPilot((sender: ActorRef, msg: Any) =>
|
||||
msg match {
|
||||
case Restart =>
|
||||
restartRequests += 1
|
||||
sender ! ServerBooted(connectionInfo, processManagerProbe.ref)
|
||||
TestActor.KeepRunning
|
||||
case _ => TestActor.KeepRunning
|
||||
}
|
||||
)
|
||||
|
||||
val parent = system.actorOf(
|
||||
Props(
|
||||
new StepParent(
|
||||
LanguageServerSupervisor.props(
|
||||
serverConfig,
|
||||
serverComponent,
|
||||
supervisionConfig,
|
||||
new AkkaBasedWebSocketConnectionFactory(),
|
||||
virtualTime.scheduler
|
||||
connectionInfo = connectionInfo,
|
||||
serverProcessManager = processManagerProbe.ref,
|
||||
supervisionConfig = supervisionConfig,
|
||||
connectionFactory = new AkkaBasedWebSocketConnectionFactory(),
|
||||
scheduler = virtualTime.scheduler
|
||||
),
|
||||
parentProbe.ref
|
||||
)
|
||||
|
@ -64,7 +64,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(
|
||||
client.expectTaskStarted()
|
||||
client.expectJsonAfterSomeProgress(
|
||||
json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
@ -84,7 +85,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(
|
||||
client.expectTaskStarted()
|
||||
client.expectJsonAfterSomeProgress(
|
||||
json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
@ -164,11 +166,15 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
val message =
|
||||
"Installation has been cancelled by the user because the requested " +
|
||||
"engine release is marked as broken."
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"id":0,
|
||||
"error": { "code": 4021, "message": "Installation has been cancelled by the user because the requested engine release is marked as broken." }
|
||||
"error": { "code": 4021, "message": $message }
|
||||
}
|
||||
""")
|
||||
|
||||
@ -182,7 +188,8 @@ class EngineManagementApiSpec extends BaseServerSpec with FlakySpec {
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(
|
||||
client.expectTaskStarted()
|
||||
client.expectJsonAfterSomeProgress(
|
||||
json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import java.nio.file.Paths
|
||||
import java.util.UUID
|
||||
|
||||
import io.circe.literal._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.projectmanager.test.Net.tryConnect
|
||||
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
|
||||
@ -22,6 +23,8 @@ class ProjectManagementApiSpec
|
||||
gen.reset()
|
||||
}
|
||||
|
||||
override val engineToInstall = Some(SemVer(0, 0, 1))
|
||||
|
||||
"project/create" must {
|
||||
|
||||
"check if project name is not empty" taggedAs Flaky in {
|
||||
@ -31,7 +34,8 @@ class ProjectManagementApiSpec
|
||||
"method": "project/create",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"name": ""
|
||||
"name": "",
|
||||
"missingComponentAction": "Install"
|
||||
}
|
||||
}
|
||||
""")
|
||||
@ -124,26 +128,23 @@ class ProjectManagementApiSpec
|
||||
}
|
||||
|
||||
"create project with specific version" in {
|
||||
// TODO [RW] this is just a stub test for parsing, it should be replaced
|
||||
// with actual tests once this functionality is implemented
|
||||
implicit val client = new WsTestClient(address)
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "project/create",
|
||||
"id": 0,
|
||||
"id": 1,
|
||||
"params": {
|
||||
"name": "foo",
|
||||
"version": "1.2.3"
|
||||
"version": "0.0.1"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc":"2.0",
|
||||
"id":0,
|
||||
"error":{
|
||||
"code":10,
|
||||
"message":"The requested method is not implemented"
|
||||
"jsonrpc" : "2.0",
|
||||
"id" : 1,
|
||||
"result" : {
|
||||
"projectId" : $getGeneratedUUID
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,3 +1,12 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
||||
jvm-options:
|
||||
- value: "-Dpolyglot.engine.IterativePartialEscape=true"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath\\component\\runtime.jar"
|
||||
os: "windows"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "linux"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "macos"
|
||||
- value: "-Denso.version.override=0.0.1"
|
||||
|
@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
||||
echo "Fake JVM executable has been executed"
|
||||
|
@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
||||
echo "Fake JVM executable has been executed"
|
||||
|
@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
||||
echo "Fake JVM executable has been executed"
|
||||
|
@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
||||
echo "Fake JVM executable has been executed"
|
||||
|
@ -4,7 +4,7 @@ import buildinfo.Info
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
/** Helper object that allows to get the current launcher version.
|
||||
/** Helper object that allows to get the current application version.
|
||||
*
|
||||
* In development-mode it allows to override the returned version for testing
|
||||
* purposes.
|
||||
|
@ -4,8 +4,13 @@ import java.nio.file.{Files, Path, StandardOpenOption}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.runtimeversionmanager.FileSystem
|
||||
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
|
||||
import org.enso.runtimeversionmanager.archive.Archive
|
||||
import org.enso.runtimeversionmanager.distribution.{
|
||||
DistributionManager,
|
||||
TemporaryDirectoryManager
|
||||
}
|
||||
import org.enso.runtimeversionmanager.locking.{
|
||||
LockType,
|
||||
Resource,
|
||||
@ -14,11 +19,6 @@ import org.enso.runtimeversionmanager.locking.{
|
||||
import org.enso.runtimeversionmanager.releases.ReleaseProvider
|
||||
import org.enso.runtimeversionmanager.releases.engine.EngineRelease
|
||||
import org.enso.runtimeversionmanager.releases.graalvm.GraalVMRuntimeReleaseProvider
|
||||
import org.enso.runtimeversionmanager.FileSystem
|
||||
import org.enso.runtimeversionmanager.distribution.{
|
||||
DistributionManager,
|
||||
TemporaryDirectoryManager
|
||||
}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Try, Using}
|
||||
|
@ -20,18 +20,7 @@ class ResourceManager(lockManager: LockManager) {
|
||||
resource: Resource,
|
||||
lockType: LockType
|
||||
)(action: => R): R = {
|
||||
var waited = false
|
||||
Using {
|
||||
lockManager.acquireLockWithWaitingAction(
|
||||
resource.name,
|
||||
lockType = lockType,
|
||||
() => {
|
||||
waited = true
|
||||
waitingInterface.startWaitingForResource(resource)
|
||||
}
|
||||
)
|
||||
} { _ =>
|
||||
if (waited) waitingInterface.finishWaitingForResource(resource)
|
||||
Using(acquireResource(waitingInterface, resource, lockType)) { _ =>
|
||||
action
|
||||
}.get
|
||||
}
|
||||
@ -52,6 +41,27 @@ class ResourceManager(lockManager: LockManager) {
|
||||
action
|
||||
}
|
||||
|
||||
/** Acquires a resource, handling possible waiting, and returns its [[Lock]]
|
||||
* instance that can be used to unlock it.
|
||||
*/
|
||||
private def acquireResource(
|
||||
waitingInterface: LockUserInterface,
|
||||
resource: Resource,
|
||||
lockType: LockType
|
||||
): Lock = {
|
||||
var waited = false
|
||||
val lock = lockManager.acquireLockWithWaitingAction(
|
||||
resource.name,
|
||||
lockType = lockType,
|
||||
() => {
|
||||
waited = true
|
||||
waitingInterface.startWaitingForResource(resource)
|
||||
}
|
||||
)
|
||||
if (waited) waitingInterface.finishWaitingForResource(resource)
|
||||
lock
|
||||
}
|
||||
|
||||
var mainLock: Option[Lock] = None
|
||||
|
||||
/** Initializes the [[MainLock]].
|
||||
|
@ -21,10 +21,7 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
|
||||
def run(): Try[Int] =
|
||||
wrapError {
|
||||
logger.debug(s"Executing $toString")
|
||||
val processBuilder = new java.lang.ProcessBuilder(command: _*)
|
||||
for ((key, value) <- extraEnv) {
|
||||
processBuilder.environment().put(key, value)
|
||||
}
|
||||
val processBuilder = builder()
|
||||
processBuilder.inheritIO()
|
||||
val process = processBuilder.start()
|
||||
process.waitFor()
|
||||
@ -44,6 +41,21 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
|
||||
processBuilder.!!
|
||||
}
|
||||
|
||||
/** Returns a [[ProcessBuilder]] that can be used to start the process.
|
||||
*
|
||||
* This is an advanced feature and it has to be used very carefully - the
|
||||
* builder and the constructed process (as long as it is running) must not
|
||||
* leak outside of the enclosing `withCommand` function to preserve the
|
||||
* guarantees that the environment the process requires still exists.
|
||||
*/
|
||||
def builder(): ProcessBuilder = {
|
||||
val processBuilder = new java.lang.ProcessBuilder(command: _*)
|
||||
for ((key, value) <- extraEnv) {
|
||||
processBuilder.environment().put(key, value)
|
||||
}
|
||||
processBuilder
|
||||
}
|
||||
|
||||
/** Runs the provided action and wraps any errors into a [[Failure]]
|
||||
* containing a [[RunnerError]].
|
||||
*/
|
||||
|
@ -2,8 +2,37 @@ package org.enso.runtimeversionmanager.runner
|
||||
|
||||
/** Represents settings that are used to launch the runtime JVM.
|
||||
*
|
||||
* @param useSystemJVM if set, the system configured JVM is used instead of
|
||||
* the one managed by the launcher
|
||||
* @param javaCommandOverride the command should be used to launch the JVM
|
||||
* instead of the default JVM provided with the
|
||||
* release; it can be an absolute path to a java
|
||||
* executable
|
||||
* @param jvmOptions options that should be added to the launched JVM
|
||||
*/
|
||||
case class JVMSettings(useSystemJVM: Boolean, jvmOptions: Seq[(String, String)])
|
||||
case class JVMSettings(
|
||||
javaCommandOverride: Option[JavaCommand],
|
||||
jvmOptions: Seq[(String, String)]
|
||||
)
|
||||
|
||||
object JVMSettings {
|
||||
|
||||
/** Creates settings that are used to launch the runtime JVM.
|
||||
*
|
||||
* @param useSystemJVM if set, the system configured JVM is used instead of
|
||||
* the one managed by the launcher
|
||||
* @param jvmOptions options that should be added to the launched JVM
|
||||
*/
|
||||
def apply(
|
||||
useSystemJVM: Boolean,
|
||||
jvmOptions: Seq[(String, String)]
|
||||
): JVMSettings =
|
||||
new JVMSettings(
|
||||
if (useSystemJVM) Some(JavaCommand.systemJavaCommand) else None,
|
||||
jvmOptions
|
||||
)
|
||||
|
||||
/** Creates a default instance of [[JVMSettings]] that just use the default
|
||||
* JVM with no options overrides.
|
||||
*/
|
||||
def default: JVMSettings =
|
||||
JVMSettings(useSystemJVM = false, jvmOptions = Seq())
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
|
||||
}
|
@ -4,13 +4,13 @@ import nl.gn0s1s.bump.SemVer
|
||||
|
||||
/** Represents settings that are used to launch the runner JAR.
|
||||
*
|
||||
* @param version Enso engine version to use
|
||||
* @param engineVersion Enso engine version to use
|
||||
* @param runnerArguments arguments that should be passed to the runner
|
||||
* @param connectLoggerIfAvailable specifies if the ran component should
|
||||
* connect to launcher's logging service
|
||||
*/
|
||||
case class RunSettings(
|
||||
version: SemVer,
|
||||
engineVersion: SemVer,
|
||||
runnerArguments: Seq[String],
|
||||
connectLoggerIfAvailable: Boolean
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user