mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 21:27:15 +03:00
Logging Service in the Launcher (#1169)
Migrate launcher's HTTP backend from Apache HTTP to Akka.
This commit is contained in:
parent
6c409958ec
commit
c824c1cb7b
89
build.sbt
89
build.sbt
@ -139,6 +139,8 @@ lazy val enso = (project in file("."))
|
||||
logger.jvm,
|
||||
pkg,
|
||||
cli,
|
||||
`logging-service`,
|
||||
`akka-native`,
|
||||
`version-output`,
|
||||
runner,
|
||||
runtime,
|
||||
@ -306,7 +308,6 @@ val zio = Seq(
|
||||
|
||||
// === Other ==================================================================
|
||||
|
||||
val apacheHttpClientVersion = "4.5.12"
|
||||
val bcpkixJdk15Version = "1.65"
|
||||
val bumpVersion = "0.1.3"
|
||||
val declineVersion = "1.2.0"
|
||||
@ -327,6 +328,7 @@ val scalameterVersion = "0.19"
|
||||
val scalatagsVersion = "0.9.1"
|
||||
val scalatestVersion = "3.3.0-SNAP2"
|
||||
val shapelessVersion = "2.4.0-M1"
|
||||
val slf4jVersion = "1.7.30"
|
||||
val slickVersion = "3.3.2"
|
||||
val sqliteVersion = "3.31.1"
|
||||
val tikaVersion = "1.24.1"
|
||||
@ -494,6 +496,44 @@ lazy val pkg = (project in file("lib/scala/pkg"))
|
||||
)
|
||||
.settings(licenseSettings)
|
||||
|
||||
lazy val `akka-native` = project
|
||||
.in(file("lib/scala/akka-native"))
|
||||
.configs(Test)
|
||||
.settings(
|
||||
version := "0.1",
|
||||
libraryDependencies ++= Seq(
|
||||
akkaActor
|
||||
),
|
||||
// Note [Native Image Workaround for GraalVM 20.2]
|
||||
libraryDependencies += "org.graalvm.nativeimage" % "svm" % graalVersion % "provided"
|
||||
)
|
||||
.settings(licenseSettings)
|
||||
|
||||
lazy val `logging-service` = project
|
||||
.in(file("lib/scala/logging-service"))
|
||||
.configs(Test)
|
||||
.settings(
|
||||
version := "0.1",
|
||||
libraryDependencies ++= Seq(
|
||||
"org.slf4j" % "slf4j-api" % slf4jVersion,
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
|
||||
akkaStream,
|
||||
akkaHttp,
|
||||
"io.circe" %%% "circe-core" % circeVersion,
|
||||
"io.circe" %%% "circe-parser" % circeVersion,
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"org.graalvm.nativeimage" % "svm" % graalVersion % "provided"
|
||||
)
|
||||
)
|
||||
.settings(
|
||||
if (Platform.isWindows)
|
||||
(Compile / unmanagedSourceDirectories) += (Compile / sourceDirectory).value / "java-windows"
|
||||
else
|
||||
(Compile / unmanagedSourceDirectories) += (Compile / sourceDirectory).value / "java-unix"
|
||||
)
|
||||
.settings(licenseSettings)
|
||||
.dependsOn(`akka-native`)
|
||||
|
||||
lazy val cli = project
|
||||
.in(file("lib/scala/cli"))
|
||||
.configs(Test)
|
||||
@ -895,19 +935,7 @@ lazy val runtime = (project in file("engine/runtime"))
|
||||
.value
|
||||
)
|
||||
.settings(
|
||||
(Test / compile) := (Test / compile)
|
||||
.dependsOn(Def.task {
|
||||
val cmd = Seq("mvn", "package", "-f", "std-bits")
|
||||
val exitCode = if (sys.props("os.name").toLowerCase().contains("win")) {
|
||||
(Seq("cmd", "/c") ++ cmd).!
|
||||
} else {
|
||||
cmd.!
|
||||
}
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException("std-bits build failed.")
|
||||
}
|
||||
})
|
||||
.value
|
||||
(Test / compile) := (Test / compile).dependsOn(StdBits.preparePackage).value
|
||||
)
|
||||
.settings(
|
||||
logBuffered := false,
|
||||
@ -1033,6 +1061,7 @@ lazy val runner = project
|
||||
.dependsOn(pkg)
|
||||
.dependsOn(`language-server`)
|
||||
.dependsOn(`polyglot-api`)
|
||||
.dependsOn(`logging-service`)
|
||||
|
||||
lazy val launcher = project
|
||||
.in(file("engine/launcher"))
|
||||
@ -1040,11 +1069,13 @@ lazy val launcher = project
|
||||
.settings(
|
||||
resolvers += Resolver.bintrayRepo("gn0s1s", "releases"),
|
||||
libraryDependencies ++= Seq(
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"org.typelevel" %% "cats-core" % catsVersion,
|
||||
"nl.gn0s1s" %% "bump" % bumpVersion,
|
||||
"org.apache.commons" % "commons-compress" % commonsCompressVersion,
|
||||
"org.apache.httpcomponents" % "httpclient" % apacheHttpClientVersion
|
||||
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
|
||||
"org.typelevel" %% "cats-core" % catsVersion,
|
||||
"nl.gn0s1s" %% "bump" % bumpVersion,
|
||||
"org.apache.commons" % "commons-compress" % commonsCompressVersion,
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
akkaHttp,
|
||||
akkaSLF4J
|
||||
)
|
||||
)
|
||||
.settings(
|
||||
@ -1053,14 +1084,18 @@ lazy val launcher = project
|
||||
"enso",
|
||||
staticOnLinux = true,
|
||||
Seq(
|
||||
"-J-Xmx4G",
|
||||
"--enable-all-security-services", // Note [HTTPS in the Launcher]
|
||||
"-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.NoOpLog",
|
||||
"-H:IncludeResources=.*Main.enso$"
|
||||
"-H:IncludeResources=.*Main.enso$",
|
||||
"--initialize-at-run-time=" +
|
||||
"akka.protobuf.DescriptorProtos," +
|
||||
"com.typesafe.config.impl.ConfigImpl$EnvVariablesHolder," +
|
||||
"com.typesafe.config.impl.ConfigImpl$SystemPropertiesHolder," +
|
||||
"org.enso.loggingservice.WSLoggerManager$" // Note [WSLoggerManager Shutdown Hook]
|
||||
)
|
||||
)
|
||||
.value,
|
||||
// Note [Native Image Workaround for GraalVM 20.2]
|
||||
libraryDependencies += "org.graalvm.nativeimage" % "svm" % "20.2.0" % "provided",
|
||||
test in assembly := {},
|
||||
assemblyOutputPath in assembly := file("launcher.jar")
|
||||
)
|
||||
@ -1082,6 +1117,7 @@ lazy val launcher = project
|
||||
.dependsOn(cli)
|
||||
.dependsOn(`version-output`)
|
||||
.dependsOn(pkg)
|
||||
.dependsOn(`logging-service`)
|
||||
|
||||
/* Note [HTTPS in the Launcher]
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -1114,3 +1150,12 @@ lazy val launcher = project
|
||||
* as that workaround is in-place. The dependency is marked as "provided"
|
||||
* because it is included within the native-image build.
|
||||
*/
|
||||
|
||||
/* Note [WSLoggerManager Shutdown Hook]
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* As the WSLoggerManager registers a shutdown hook when its initialized to
|
||||
* ensure that logs are not lost in case of logging service initialization
|
||||
* failure, it has to be initialized at runtime, as otherwise if the
|
||||
* initialization was done at build time, the shutdown hook would actually also
|
||||
* run at build time and have no effect at runtime.
|
||||
*/
|
||||
|
@ -170,36 +170,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Julian Seward, jseward@acm.org
|
||||
|
||||
-----------------------
|
||||
|
||||
Apache HttpClient, licensed under Apache License Version 2.0
|
||||
(see: components-licences/LICENSE-APACHE) is distributed with the launcher.
|
||||
It requires the following notice:
|
||||
|
||||
Apache HttpClient
|
||||
Copyright 1999-2020 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
|
||||
-----------------------
|
||||
|
||||
Apache HttpCore, licensed under Apache License Version 2.0
|
||||
(see: components-licences/LICENSE-APACHE) is distributed with the launcher.
|
||||
It requires the following notice:
|
||||
|
||||
Apache HttpCore
|
||||
Copyright 2005-2020 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
|
||||
-----------------------
|
||||
|
||||
|
||||
|
||||
=======================
|
||||
|
||||
As the launcher is written in Scala and built with Graal Native Image, its
|
||||
@ -238,11 +208,13 @@ Copyright Erik Osheim, 2012-2020.
|
||||
|
||||
'shapeless', licensed under the Apache License, Version 2.0, is distributed with
|
||||
the launcher.
|
||||
Copyright (c) 2018 Miles Sabin
|
||||
|
||||
================
|
||||
|
||||
'snakeyaml', licensed under the Apache License, Version 2.0, is distributed with
|
||||
the launcher.
|
||||
the launcher. It requires the following notice:
|
||||
Copyright (c) 2008, http://www.snakeyaml.org
|
||||
|
||||
================
|
||||
|
||||
@ -281,6 +253,81 @@ limitations under the License.
|
||||
|
||||
================
|
||||
|
||||
The module 'scala-java8-compat' licensed under the Apache License, Version 2.0,
|
||||
is distributed with the launcher. It requires the following notice:
|
||||
scala-java8-compat
|
||||
Copyright (c) 2002-2020 EPFL
|
||||
Copyright (c) 2011-2020 Lightbend, Inc.
|
||||
|
||||
scala-java8-compat includes software developed at
|
||||
LAMP/EPFL (https://lamp.epfl.ch/) and
|
||||
Lightbend, Inc. (https://www.lightbend.com/).
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License").
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================
|
||||
|
||||
'akka-http', 'akka-stream' and 'akka-actor' which also include 'akka-http-core',
|
||||
'akka-parsing', 'akka-protobuf-v3' licensed under
|
||||
the Apache License, Version 2.0, are distributed with the launcher.
|
||||
Their license is located at `components-licences/LICENSE-AKKA`.
|
||||
|
||||
================
|
||||
|
||||
'reactive-streams', licensed under CC0, is distributed with the launcher.
|
||||
|
||||
================
|
||||
|
||||
'ssl-config-core', licensed under the Apache License, Version 2.0, is
|
||||
distributed with the launcher. It requires the following notice:
|
||||
Copyright (C) 2015 - 2020 Lightbend Inc. <https://www.lightbend.com>
|
||||
|
||||
================
|
||||
|
||||
'config', licensed under the Apache License, Version 2.0, is distributed with
|
||||
the launcher. It requires the following notice:
|
||||
Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com>
|
||||
Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
|
||||
|
||||
================
|
||||
|
||||
'scala-logging', licensed under the Apache License, Version 2.0, is distributed
|
||||
with the launcher. It requires the following notice:
|
||||
Copyright 2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
|
||||
================
|
||||
|
||||
'slf4j-api', licensed under the MIT license, is distributed with the launcher.
|
||||
It requires the following notice:
|
||||
Copyright (c) 2004-2017 QOS.ch
|
||||
All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================
|
||||
|
||||
The version of this launcher built for the Linux platform uses `zlib` created by
|
||||
Jean-loup Gailly and Mark Adler.
|
||||
|
||||
|
33
distribution/launcher/components-licences/COPYING.protobuf
Normal file
33
distribution/launcher/components-licences/COPYING.protobuf
Normal file
@ -0,0 +1,33 @@
|
||||
Copyright 2008, Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 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
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Code generated by the Protocol Buffer compiler is owned by the owner
|
||||
of the input file used when generating it. This code is not
|
||||
standalone and requires a support library to be linked with it. This
|
||||
support library is itself covered by the above license.
|
212
distribution/launcher/components-licences/LICENSE-AKKA
Normal file
212
distribution/launcher/components-licences/LICENSE-AKKA
Normal file
@ -0,0 +1,212 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) 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. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
---------------
|
||||
|
||||
Licenses for dependency projects can be found here:
|
||||
[http://akka.io/docs/akka/snapshot/project/licenses.html]
|
||||
|
||||
---------------
|
||||
|
||||
akka-protobuf contains the sources of Google protobuf 2.5.0 runtime support,
|
||||
moved into the source package `akka.protobuf` so as to avoid version conflicts.
|
||||
For license information see COPYING.protobuf
|
@ -19,4 +19,8 @@ up as follows:
|
||||
used for building the launcher native binary.
|
||||
- [**Rust:**](rust.md) Description of integration of the Scala project with the
|
||||
Rust components.
|
||||
- [**Upgrading GraalVM:**](upgrading-graalvm.md)
|
||||
- [**Upgrading GraalVM:**](upgrading-graalvm.md) Description of steps that have
|
||||
to be performed by each developer when the project is upgraded to a new
|
||||
version of GraalVM.
|
||||
- [**Logging**:](logging.md) Description of an unified and centralized logging
|
||||
infrastructure that should be used by all components.
|
||||
|
253
docs/infrastructure/logging.md
Normal file
253
docs/infrastructure/logging.md
Normal file
@ -0,0 +1,253 @@
|
||||
---
|
||||
layout: developer-doc
|
||||
title: Logging Service
|
||||
category: infrastructure
|
||||
tags: [infrastructure, logging, debug]
|
||||
order: 6
|
||||
---
|
||||
|
||||
# Logging
|
||||
|
||||
The Enso project features a centralised logging service to allow for the
|
||||
aggregation of logs from multiple components. This service can be started with
|
||||
one of the main components, allowing other components connect to it. The service
|
||||
aggregates all logs in one place for easier analysis of the interaction between
|
||||
components.
|
||||
|
||||
<!-- MarkdownTOC levels="2,3" autolink="true" -->
|
||||
|
||||
- [Protocol](#protocol)
|
||||
- [Types](#types)
|
||||
- [Messages](#messages)
|
||||
- [Examples](#examples)
|
||||
- [JVM Architecture](#jvm-architecture)
|
||||
- [SLF4J Interface](#slf4j-interface)
|
||||
- [Setting Up Logging](#setting-up-logging)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
## Protocol
|
||||
|
||||
The service relies on a WebSocket connection to a specified endpoint that
|
||||
exchanges JSON-encoded text messages. The communication is uni-directional - the
|
||||
only messages are log messages that are sent from a connected client to the
|
||||
server that aggregates the logs.
|
||||
|
||||
### Types
|
||||
|
||||
##### `LogLevel`
|
||||
|
||||
The log level encoded as a number. Possible values are:
|
||||
|
||||
- 0 - indicating `ERROR` level,
|
||||
- 1 - indicating `WARN` level,
|
||||
- 2 - indicating `INFO` level,
|
||||
- 3 - indicating `DEBUG` level,
|
||||
- 4 - indicating `TRACE` level.
|
||||
|
||||
```typescript
|
||||
type LogLevel = 0 | 1 | 2 | 3 | 4;
|
||||
```
|
||||
|
||||
##### `UTCTime`
|
||||
|
||||
Message timestamp encoded as milliseconds elapsed from the UNIX epoch, i.e.
|
||||
1970-01-01T00:00:00Z.
|
||||
|
||||
```typescript
|
||||
type UTCTime = number;
|
||||
```
|
||||
|
||||
##### `Exception`
|
||||
|
||||
Encodes an exception that is related to a log message.
|
||||
|
||||
The `cause` field may be omitted if the exception does not have another
|
||||
exception as its cause.
|
||||
|
||||
```typescript
|
||||
interface Exception {
|
||||
// Name of the exception. In Java this can be the qualified classname.
|
||||
name: String;
|
||||
// Message associated with the exception. May be empty.
|
||||
message: String;
|
||||
// A stack trace indicating code location where the exception has originated
|
||||
// from. May be empty if unavailable.
|
||||
trace: [TraceElement];
|
||||
// Optional, another exception that caused this one.
|
||||
cause?: Exception;
|
||||
}
|
||||
```
|
||||
|
||||
##### `TraceElement`
|
||||
|
||||
Represents a single element of exception's stacktrace.
|
||||
|
||||
```typescript
|
||||
interface TraceElement {
|
||||
// Name of the stack location. For example, in Java this can be a qualified
|
||||
// method name.
|
||||
element: String;
|
||||
// Code location of the element.
|
||||
location: String;
|
||||
}
|
||||
```
|
||||
|
||||
In Java, the location is usually a filename and line number locating the code
|
||||
that corresponds to the indicated stack location, for example `Main.java:123`.
|
||||
Native methods may be handled differently, as well as code from different
|
||||
languages, for example Enso also includes the columns - `Test.enso:4:3-19`.
|
||||
|
||||
### Messages
|
||||
|
||||
Currently, the service supports only one message type - `LogMessage`, messages
|
||||
not conforming to this format will be ignored. The first non-conforming message
|
||||
for each connection will emit a warning.
|
||||
|
||||
#### `LogMessage`
|
||||
|
||||
Describes the log message that the server should report and does not expect any
|
||||
response.
|
||||
|
||||
##### Parameters
|
||||
|
||||
```typescript
|
||||
{
|
||||
// Log level associated with the message.
|
||||
level: LogLevel;
|
||||
// Timestamp indicating when the message was sent.
|
||||
time: UTCTime;
|
||||
// An identifier of a log group - the group should indicate which component
|
||||
// the message originated from and any (possibly nested) context.
|
||||
group: String;
|
||||
// The actual log message.
|
||||
message: String;
|
||||
// Optional exception associated with the message.
|
||||
exception?: Exception;
|
||||
}
|
||||
```
|
||||
|
||||
The `exception` field may be omitted if there is no exception associated with
|
||||
the message.
|
||||
|
||||
In general, the `group` name can be arbitrary, but it is often the quallified
|
||||
name of the class that the log message originates from and it is sometimes
|
||||
extended with additional nested context, for example:
|
||||
|
||||
- `org.enso.launcher.cli.Main`
|
||||
- `org.enso.compiler.pass.analyse.AliasAnalysis.analyseType`
|
||||
|
||||
### Examples
|
||||
|
||||
For example, an error message with an attached exception may look like this (the
|
||||
class names are made up):
|
||||
|
||||
```json
|
||||
{
|
||||
"level": 0,
|
||||
"time": 1600864353151,
|
||||
"group": "org.enso.launcher.Main",
|
||||
"message": "Failed to load a configuration file.",
|
||||
"exception": {
|
||||
"name": "org.enso.launcher.config.ConfigurationLoaderFailure",
|
||||
"message": "Configuration file does not exist.",
|
||||
"trace": [
|
||||
{
|
||||
"element": "org.enso.launcher.config.ConfigurationLoader.load",
|
||||
"location": "ConfigurationLoader.scala:123"
|
||||
},
|
||||
{
|
||||
"element": "org.enso.launcher.Main",
|
||||
"location": "Main.scala:42"
|
||||
}
|
||||
],
|
||||
"cause": {
|
||||
"name": "java.io.FileNotFoundException",
|
||||
"message": "config.yaml (No such file or directory)",
|
||||
"trace": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Another example could be an info message (without attached exceptions):
|
||||
|
||||
```json
|
||||
{
|
||||
"level": 2,
|
||||
"time": 1600864353151,
|
||||
"group": "org.enso.launcher.Main",
|
||||
"message": "Configuration file loaded successfully."
|
||||
}
|
||||
```
|
||||
|
||||
## JVM Architecture
|
||||
|
||||
A default implementation of both a client and server for the logger service are
|
||||
provided for the JVM.
|
||||
|
||||
### SLF4J Interface
|
||||
|
||||
The `logging-service` provides a class `org.enso.loggingservice.WSLogger` which
|
||||
implements the `org.slf4j.Logger` interface, so it is compatible with all code
|
||||
using SLF4J logging. When the `logging-service` is added to a project, it
|
||||
automatically binds its logger instance as the SLF4J backend. So from the
|
||||
perspective of the user, all that they have to do is use SLF4J compliant logging
|
||||
in the application.
|
||||
|
||||
One can use the `org.slf4j.LoggerFactory` directly, but for Scala code, it is
|
||||
much better to use the `com.typesafe.scalalogging.Logger` which wraps the SLF4J
|
||||
logger with macros that compute the log messages only if the given logging level
|
||||
is enabled, and allows much prettier initialisation. Additionally, the
|
||||
`logging-service` provides syntactic sugar for working with nested logging
|
||||
contexts.
|
||||
|
||||
```
|
||||
package foo
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.logger.LoggerSyntax
|
||||
|
||||
class Foo {
|
||||
private val logger = Logger[Foo]
|
||||
|
||||
def bar(): Unit = {
|
||||
logger.info("Hello world") // Logs `Hello world` from context `foo.Foo`.
|
||||
baz()
|
||||
}
|
||||
|
||||
def baz(): Unit = {
|
||||
val bazLogger = logger.enter("baz")
|
||||
bazLogger.warn("Inner") // Logs `Inner` from context `foo.Foo.baz`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `enter` extension method follows the convention that each level of context
|
||||
nesting is separated by `.`, much like package names. The root context is
|
||||
usually the qualified name of the relevant class, but other components are free
|
||||
to use other conventions if needed.
|
||||
|
||||
### Setting Up Logging
|
||||
|
||||
The logger described above must know where it should send its logs, and this is
|
||||
handled by the `LoggingServiceManager`. It allows to configure the logging
|
||||
location, log level and setup the logging service in one of three different
|
||||
modes:
|
||||
|
||||
- _Server mode_, that will listen on a given port, gather both local and remote
|
||||
logs and print them to stderr and to a file.
|
||||
- _Client mode_, that will connect to a specified server and send all of its
|
||||
logs there. It will not print anything.
|
||||
- _Fallback mode_, that will just write the logs to stderr (and optionally) a
|
||||
file, without setting up any services or connections.
|
||||
|
||||
This logging mode initialization cannot usually happen at the time of static
|
||||
initialization, since the connection details may depend on CLI arguments or
|
||||
other configuration which may not be accessed immediately. To help with this,
|
||||
the logger will buffer any log messages that are issued before the
|
||||
initialization has happened and send them as soon as the service is initialized.
|
||||
|
||||
In a rare situation where the service would not be initialized at all, a
|
||||
shutdown hook is added that will print the pending log messages before exiting.
|
||||
Some of the messages may be dropped, however, if more messages are buffered than
|
||||
the buffer can hold.
|
@ -73,7 +73,7 @@ As the Native Image builds a native binary, certain capabilities, like
|
||||
[reflection](https://github.com/oracle/graal/blob/master/substratevm/REFLECTION.md),
|
||||
may be limited. The build system tries to automatically detect some reflective
|
||||
accesses, but it cannot detect everything. It is possible for the built binary
|
||||
to fail with the following error:
|
||||
to fail with `java.lang.ClassNotFoundException` or the following error:
|
||||
|
||||
```
|
||||
java.lang.InstantiationException: Type `XYZ` can not be instantiated reflectively as it does not have a no-parameter constructor or the no-parameter constructor has not been added explicitly to the native image.`
|
||||
@ -117,7 +117,44 @@ It is possible that different classes are reflectively accessed on different
|
||||
platforms. In that case it may be necessary to run the agent on multiple
|
||||
platforms and merge the configs. If the conflicts were conflicting (i.e. some
|
||||
reflectively accessed classes existed only on one platform), it may be necessary
|
||||
to maintain separate configs for each platform. Currently in the Launcher this
|
||||
is not the case - the reflective accesses seem to be platform independent, as
|
||||
the launcher built with a config created on Linux runs successfully on other
|
||||
platforms.
|
||||
to maintain separate configs for each platform. Currently, the
|
||||
`native-image-agent` is not available on Windows, so Windows-specific reflective
|
||||
accesses may have to be gathered manually. For some types of accesses it may be
|
||||
possible to force the Windows-specific code paths to run on Linux and gather
|
||||
these accesses semi-automatically.
|
||||
|
||||
### Launcher Configuration
|
||||
|
||||
In case of the launcher, to gather the relevant reflective accesses one wants to
|
||||
test as many execution paths as possible, especially the ones that are likely to
|
||||
use reflection. One of these areas is HTTP support and archive extraction.
|
||||
|
||||
To trace this accesses, it is good to run at least
|
||||
`... launcher.jar install engine` which will trigger HTTP downloads and archive
|
||||
extraction.
|
||||
|
||||
Currently, archive-related accesses are platform dependent - Linux launcher only
|
||||
uses `.tar.gz` and Windows uses `.zip`. While the Linux launcher never unpacks
|
||||
ZIP files, we can manually force it to do so, to register the reflection
|
||||
configuration that will than be used on Windows to enable ZIP extraction.
|
||||
|
||||
To force the launcher to extract a ZIP on Linux, one can add the following code
|
||||
snippet (with the necessary imports) to `org.enso.launcher.cli.Main.main`:
|
||||
|
||||
```
|
||||
Archive.extractArchive(Path.of("enso-engine-windows.zip"), Path.of("somewhere"), None)
|
||||
```
|
||||
|
||||
With this snippet, `launcher.jar` should be built using the
|
||||
`launcher / assembly` task, and the tracing tool should be re-run as shown
|
||||
above.
|
||||
|
||||
Moreover, some reflective accesses may not be detected by the tool
|
||||
automatically, so they may need to be added manually. One of them is an access
|
||||
to the class `[B` when using Akka, so it would require manually adding it to the
|
||||
`reflect-config.json`. This strange looking access is most likely reflective
|
||||
access to an array of bytes. To make it easier, a package `akka-native` has been
|
||||
created that gathers workarounds required to be able to build native images
|
||||
using Akka, so it is enough to just add it as a dependency. It does not handle
|
||||
other reflective accesses that are related to Akka, because the ones that are
|
||||
needed are gathered automatically using the tool described above.
|
||||
|
@ -1,6 +1,395 @@
|
||||
[
|
||||
{
|
||||
"name": "java.lang.String"
|
||||
"name": "akka.actor.ActorCell",
|
||||
"fields": [
|
||||
{
|
||||
"name": "akka$actor$dungeon$Children$$_childrenRefsDoNotCallMeDirectly",
|
||||
"allowUnsafeAccess": true
|
||||
},
|
||||
{
|
||||
"name": "akka$actor$dungeon$Children$$_functionRefsDoNotCallMeDirectly",
|
||||
"allowUnsafeAccess": true
|
||||
},
|
||||
{
|
||||
"name": "akka$actor$dungeon$Children$$_nextNameDoNotCallMeDirectly",
|
||||
"allowUnsafeAccess": true
|
||||
},
|
||||
{
|
||||
"name": "akka$actor$dungeon$Dispatch$$_mailboxDoNotCallMeDirectly",
|
||||
"allowUnsafeAccess": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.DefaultSupervisorStrategy",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.LightArrayRevolverScheduler",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"com.typesafe.config.Config",
|
||||
"akka.event.LoggingAdapter",
|
||||
"java.util.concurrent.ThreadFactory"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.LightArrayRevolverScheduler$TaskHolder",
|
||||
"fields": [{ "name": "task", "allowUnsafeAccess": true }]
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.LightArrayRevolverScheduler$TaskQueue[]"
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.LocalActorRefProvider",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"java.lang.String",
|
||||
"akka.actor.ActorSystem$Settings",
|
||||
"akka.event.EventStream",
|
||||
"akka.actor.DynamicAccess"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.LocalActorRefProvider$Guardian",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.LocalActorRefProvider$SystemGuardian",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.Props$EmptyActor",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "akka.actor.RepointableActorRef",
|
||||
"fields": [
|
||||
{ "name": "_cellDoNotCallMeDirectly", "allowUnsafeAccess": true },
|
||||
{ "name": "_lookupDoNotCallMeDirectly", "allowUnsafeAccess": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.AbstractNodeQueue",
|
||||
"fields": [
|
||||
{ "name": "_tailDoNotCallMeDirectly", "allowUnsafeAccess": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.AbstractNodeQueue$Node",
|
||||
"fields": [
|
||||
{ "name": "_nextDoNotCallMeDirectly", "allowUnsafeAccess": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.BoundedControlAwareMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.BoundedDequeBasedMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.BoundedMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.ControlAwareMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.DequeBasedMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.Mailbox",
|
||||
"fields": [
|
||||
{ "name": "_statusDoNotCallMeDirectly", "allowUnsafeAccess": true },
|
||||
{ "name": "_systemQueueDoNotCallMeDirectly", "allowUnsafeAccess": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.MessageDispatcher",
|
||||
"fields": [
|
||||
{ "name": "_inhabitantsDoNotCallMeDirectly", "allowUnsafeAccess": true },
|
||||
{
|
||||
"name": "_shutdownScheduleDoNotCallMeDirectly",
|
||||
"allowUnsafeAccess": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.MultipleConsumerSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.UnboundedControlAwareMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.UnboundedDequeBasedMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.UnboundedMailbox",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"akka.actor.ActorSystem$Settings",
|
||||
"com.typesafe.config.Config"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.dispatch.UnboundedMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.event.DeadLetterListener",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "akka.event.DefaultLoggingFilter",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"akka.actor.ActorSystem$Settings",
|
||||
"akka.event.EventStream"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.event.EventStreamUnsubscriber",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.event.LoggerMailboxType",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"akka.actor.ActorSystem$Settings",
|
||||
"com.typesafe.config.Config"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.event.LoggerMessageQueueSemantics"
|
||||
},
|
||||
{
|
||||
"name": "akka.event.slf4j.Slf4jLogger",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "akka.event.slf4j.Slf4jLoggingFilter",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": [
|
||||
"akka.actor.ActorSystem$Settings",
|
||||
"akka.event.EventStream"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.http.DefaultParsingErrorHandler$",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "akka.http.impl.engine.client.PoolMasterActor",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "akka.io.InetAddressDnsProvider",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "akka.io.InetAddressDnsResolver",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.io.SelectionHandler",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.io.SimpleDnsManager",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.io.TcpIncomingConnection",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.io.TcpListener",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.io.TcpManager",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.io.TcpOutgoingConnection",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.io.dns.RecordType[]"
|
||||
},
|
||||
{
|
||||
"name": "akka.pattern.PromiseActorRef",
|
||||
"fields": [
|
||||
{ "name": "_stateDoNotCallMeDirectly", "allowUnsafeAccess": true },
|
||||
{ "name": "_watchedByDoNotCallMeDirectly", "allowUnsafeAccess": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.routing.ConsistentHashingPool",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["com.typesafe.config.Config"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.routing.ConsistentRoutee[]"
|
||||
},
|
||||
{
|
||||
"name": "akka.routing.RoundRobinPool",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["com.typesafe.config.Config"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.routing.RoutedActorCell$RouterActorCreator",
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.BooleanSerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.ByteArraySerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.ByteStringSerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.DisabledJavaSerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.IntSerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.LongSerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.SerializationExtension$",
|
||||
"fields": [{ "name": "MODULE$" }]
|
||||
},
|
||||
{
|
||||
"name": "akka.serialization.StringSerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.SinkRef"
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.SourceRef"
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.SystemMaterializer$",
|
||||
"fields": [{ "name": "MODULE$" }]
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.impl.BatchingInputBuffer[]"
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.impl.FanOut$FanoutOutputs[]"
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.impl.streamref.StreamRefsProtocol"
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.serialization.StreamRefSerializer",
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["akka.actor.ExtendedActorSystem"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "akka.stream.stage.GraphStageLogic[]"
|
||||
},
|
||||
{
|
||||
"name": "akka.util.ByteString$ByteString1"
|
||||
},
|
||||
{
|
||||
"name": "akka.util.ByteString$ByteString1C"
|
||||
},
|
||||
{
|
||||
"name": "akka.util.ByteString$ByteStrings"
|
||||
},
|
||||
{
|
||||
"name": "com.sun.crypto.provider.AESCipher$General",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "com.sun.crypto.provider.DHParameters",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "com.sun.crypto.provider.TlsKeyMaterialGenerator",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "com.sun.crypto.provider.TlsMasterSecretGenerator",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "com.sun.crypto.provider.TlsPrfGenerator$V12",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "com.typesafe.sslconfig.ssl.NoopHostnameVerifier",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "java.io.Serializable"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.Boolean"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.Class[]"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.Integer"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.Long"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.String",
|
||||
"fields": [{ "name": "value", "allowUnsafeAccess": true }]
|
||||
},
|
||||
{
|
||||
"name": "java.lang.String[]"
|
||||
@ -8,12 +397,55 @@
|
||||
{
|
||||
"name": "java.lang.invoke.VarHandle"
|
||||
},
|
||||
{
|
||||
"name": "java.nio.file.Path[]"
|
||||
},
|
||||
{
|
||||
"name": "java.security.AlgorithmParametersSpi"
|
||||
},
|
||||
{
|
||||
"name": "java.security.KeyStoreSpi"
|
||||
},
|
||||
{
|
||||
"name": "java.security.MessageDigestSpi"
|
||||
},
|
||||
{
|
||||
"name": "java.security.SecureRandomParameters"
|
||||
},
|
||||
{
|
||||
"name": "java.security.interfaces.ECPrivateKey"
|
||||
},
|
||||
{
|
||||
"name": "java.security.interfaces.ECPublicKey"
|
||||
},
|
||||
{
|
||||
"name": "java.security.interfaces.RSAPrivateKey"
|
||||
},
|
||||
{
|
||||
"name": "java.security.interfaces.RSAPublicKey"
|
||||
},
|
||||
{
|
||||
"name": "java.sql.Date"
|
||||
},
|
||||
{
|
||||
"name": "java.sql.Timestamp"
|
||||
},
|
||||
{
|
||||
"name": "java.util.Date"
|
||||
},
|
||||
{
|
||||
"name": "javax.net.ssl.KeyManager[]"
|
||||
},
|
||||
{
|
||||
"name": "javax.net.ssl.TrustManager[]"
|
||||
},
|
||||
{
|
||||
"name": "javax.security.auth.x500.X500Principal",
|
||||
"fields": [{ "name": "thisX500Name" }],
|
||||
"methods": [
|
||||
{ "name": "<init>", "parameterTypes": ["sun.security.x509.X500Name"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.AsiExtraField",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
@ -71,18 +503,224 @@
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.LogFactory"
|
||||
"name": "scala.Boolean"
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.impl.LogFactoryImpl",
|
||||
"name": "scala.Int"
|
||||
},
|
||||
{
|
||||
"name": "scala.Long"
|
||||
},
|
||||
{
|
||||
"name": "scala.Tuple2[]"
|
||||
},
|
||||
{
|
||||
"name": "sun.misc.Unsafe",
|
||||
"allDeclaredFields": true
|
||||
},
|
||||
{
|
||||
"name": "sun.security.pkcs.SignerInfo[]"
|
||||
},
|
||||
{
|
||||
"name": "sun.security.pkcs12.PKCS12KeyStore",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.impl.NoOpLog",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": ["java.lang.String"] }]
|
||||
"name": "sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.impl.WeakHashtable",
|
||||
"name": "sun.security.provider.DSA$SHA224withDSA",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.DSA$SHA256withDSA",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.JavaKeyStore$DualFormatJKS",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.JavaKeyStore$JKS",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.NativePRNG",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.SHA",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.SHA2$SHA224",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.SHA2$SHA256",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.SHA5$SHA384",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.SHA5$SHA512",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.X509Factory",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.provider.certpath.PKIXCertPathValidator",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.rsa.RSAKeyFactory$Legacy",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.rsa.RSAPSSSignature",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.rsa.RSASignature$SHA224withRSA",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.rsa.RSASignature$SHA256withRSA",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.ssl.KeyManagerFactoryImpl$SunX509",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.ssl.SSLContextImpl$DefaultSSLContext",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.ssl.SSLContextImpl$TLS12Context",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.util.ObjectIdentifier"
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.AuthorityInfoAccessExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.AuthorityKeyIdentifierExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.BasicConstraintsExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.CRLDistributionPointsExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.CertificateExtensions"
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.CertificatePoliciesExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.ExtendedKeyUsageExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.IssuerAlternativeNameExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.KeyUsageExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.NetscapeCertTypeExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.PrivateKeyUsageExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.SubjectAlternativeNameExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sun.security.x509.SubjectKeyIdentifierExtension",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": ["java.lang.Boolean", "java.lang.Object"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -1,8 +1,11 @@
|
||||
{
|
||||
"resources": [
|
||||
{ "pattern": "\\QMain.enso\\E" },
|
||||
{ "pattern": "\\Qmozilla/public-suffix-list.txt\\E" },
|
||||
{ "pattern": "\\Qorg/apache/http/client/version.properties\\E" }
|
||||
{ "pattern": "\\QMETA-INF/MANIFEST.MF\\E" },
|
||||
{ "pattern": "\\Qakka-http-version.conf\\E" },
|
||||
{ "pattern": "\\Qapplication.conf\\E" },
|
||||
{ "pattern": "\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" },
|
||||
{ "pattern": "\\Qreference.conf\\E" },
|
||||
{ "pattern": "\\Qversion.conf\\E" }
|
||||
],
|
||||
"bundles": []
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
package org.enso.launcher.workarounds;
|
||||
|
||||
import com.oracle.svm.core.annotate.Substitute;
|
||||
import com.oracle.svm.core.annotate.TargetClass;
|
||||
|
||||
/**
|
||||
* Uses Native Image substitution capability to substitute the
|
||||
* {@link scala.runtime.Statics#releaseFence()} function which causes problems
|
||||
* when building the Native Image on GraalVM 20.2.0.
|
||||
*/
|
||||
@TargetClass(className = "scala.runtime.Statics")
|
||||
final class ReplacementStatics {
|
||||
|
||||
/**
|
||||
* Implements a "release fence" without using an unsupported
|
||||
* {@link java.lang.invoke.MethodHandle} like the original one.
|
||||
*
|
||||
* Instead, uses {@link sun.misc.Unsafe#storeFence()} under the hood.
|
||||
*/
|
||||
@Substitute
|
||||
public static void releaseFence() {
|
||||
Unsafe.unsafeInstance().storeFence();
|
||||
}
|
||||
}
|
6
engine/launcher/src/main/resources/application.conf
Normal file
6
engine/launcher/src/main/resources/application.conf
Normal file
@ -0,0 +1,6 @@
|
||||
akka {
|
||||
loggers = ["akka.event.slf4j.Slf4jLogger"]
|
||||
logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
|
||||
version = "2.6.6"
|
||||
stdout-loglevel = "ERROR"
|
||||
}
|
@ -2,6 +2,7 @@ package org.enso.launcher
|
||||
|
||||
import buildinfo.Info
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import com.typesafe.scalalogging.Logger
|
||||
|
||||
/**
|
||||
* Helper object that allows to get the current launcher version.
|
||||
@ -33,7 +34,7 @@ object CurrentVersion {
|
||||
"release build."
|
||||
)
|
||||
else {
|
||||
Logger.debug(s"[TEST] Overriding version to $newVersion.")
|
||||
Logger("TEST").debug(s"Overriding version to $newVersion.")
|
||||
currentVersion = newVersion
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ package org.enso.launcher
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
@ -44,7 +46,7 @@ trait Environment {
|
||||
def parsePathWithWarning(str: String): Option[Path] = {
|
||||
val result = safeParsePath(str)
|
||||
if (result.isEmpty) {
|
||||
Logger.warn(
|
||||
Logger[Environment].warn(
|
||||
s"System variable `$key` was set (to value `$str`), but it did not " +
|
||||
s"represent a valid path, so it has been ignored."
|
||||
)
|
||||
@ -181,7 +183,7 @@ object Environment extends Environment {
|
||||
"in a release build."
|
||||
)
|
||||
else {
|
||||
Logger.debug(s"[TEST] Overriding location to $newLocation.")
|
||||
Logger("TEST").debug(s"Overriding location to $newLocation.")
|
||||
executablePathOverride = Some(newLocation)
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import org.apache.commons.io.FileUtils
|
||||
import scala.collection.Factory
|
||||
import scala.jdk.StreamConverters._
|
||||
import scala.util.Using
|
||||
import com.typesafe.scalalogging.Logger
|
||||
|
||||
/**
|
||||
* Gathers some helper methods that are used for interaction with the
|
||||
@ -22,6 +23,8 @@ import scala.util.Using
|
||||
*/
|
||||
object FileSystem {
|
||||
|
||||
private val logger = Logger[FileSystem.type]
|
||||
|
||||
/**
|
||||
* Returns a sequence of files in the given directory (without traversing it
|
||||
* recursively).
|
||||
@ -58,7 +61,7 @@ object FileSystem {
|
||||
def ensureIsExecutable(file: Path): Unit = {
|
||||
if (!Files.isExecutable(file)) {
|
||||
if (OS.isWindows) {
|
||||
Logger.error("Cannot ensure the launcher binary is executable.")
|
||||
logger.error("Cannot ensure that the binary is executable.")
|
||||
} else {
|
||||
try {
|
||||
Files.setPosixFilePermissions(
|
||||
@ -67,8 +70,8 @@ object FileSystem {
|
||||
)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
Logger.error(
|
||||
s"Cannot ensure the launcher binary is executable: $e",
|
||||
logger.error(
|
||||
s"Cannot ensure the binary is executable: $e",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.cli.CLIOutput
|
||||
|
||||
/**
|
||||
* Handles displaying of user-facing information.
|
||||
*
|
||||
* Info-level messages are used to communicate with the user. They are handled
|
||||
* in a special way, so that they are displayed to the user regardless of
|
||||
* logging settings.
|
||||
*/
|
||||
object InfoLogger {
|
||||
|
||||
private val logger = Logger("launcher")
|
||||
|
||||
/**
|
||||
* Prints an info level message.
|
||||
*
|
||||
* If the default logger is set-up to display info-messages, they are send to
|
||||
* the logger, otherwise they are printed to stdout.
|
||||
*
|
||||
* It is important to note that these messages should always be displayed to
|
||||
* the user, so unless run in debug mode, all launcher settings should ensure
|
||||
* that info-level logs are printed to the console output.
|
||||
*/
|
||||
def info(msg: => String): Unit = {
|
||||
if (logger.underlying.isInfoEnabled) {
|
||||
logger.info(msg)
|
||||
} else {
|
||||
CLIOutput.println(msg)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,9 +2,10 @@ package org.enso.launcher
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import io.circe.Json
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.cli.{GlobalCLIOptions, LauncherLogging, Main}
|
||||
import org.enso.launcher.components.ComponentsManager
|
||||
import org.enso.launcher.components.runner.{
|
||||
JVMSettings,
|
||||
@ -21,6 +22,7 @@ import org.enso.launcher.installation.{
|
||||
}
|
||||
import org.enso.launcher.project.ProjectManager
|
||||
import org.enso.launcher.upgrade.LauncherUpgrader
|
||||
import org.enso.loggingservice.LogLevel
|
||||
import org.enso.version.{VersionDescription, VersionDescriptionParameter}
|
||||
|
||||
/**
|
||||
@ -30,6 +32,8 @@ import org.enso.version.{VersionDescription, VersionDescriptionParameter}
|
||||
* @param cliOptions the global CLI options to use for the commands
|
||||
*/
|
||||
case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
private val logger = Logger[Launcher]
|
||||
|
||||
private lazy val componentsManager = ComponentsManager.default(cliOptions)
|
||||
private lazy val configurationManager =
|
||||
new GlobalConfigurationManager(componentsManager, DistributionManager)
|
||||
@ -39,7 +43,8 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
projectManager,
|
||||
configurationManager,
|
||||
componentsManager,
|
||||
Environment
|
||||
Environment,
|
||||
LauncherLogging.loggingServiceEndpoint()
|
||||
)
|
||||
private lazy val upgrader = LauncherUpgrader.default(cliOptions)
|
||||
upgrader.runCleanup(isStartup = true)
|
||||
@ -85,9 +90,11 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
}
|
||||
|
||||
if (exitCode == 0) {
|
||||
Logger.info(s"Project created in `$actualPath` using version $version.")
|
||||
InfoLogger.info(
|
||||
s"Project created in `$actualPath` using version $version."
|
||||
)
|
||||
} else {
|
||||
Logger.error("Project creation failed.")
|
||||
logger.error("Project creation failed.")
|
||||
}
|
||||
|
||||
exitCode
|
||||
@ -144,7 +151,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
def installEngine(version: SemVer): Int = {
|
||||
val existing = componentsManager.findEngine(version)
|
||||
if (existing.isDefined) {
|
||||
Logger.info(s"Engine $version is already installed.")
|
||||
InfoLogger.info(s"Engine $version is already installed.")
|
||||
} else {
|
||||
componentsManager.findOrInstallEngine(version, complain = false)
|
||||
}
|
||||
@ -158,7 +165,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
*/
|
||||
def installLatestEngine(): Int = {
|
||||
val latest = componentsManager.fetchLatestEngineVersion()
|
||||
Logger.info(s"Installing Enso engine $latest.")
|
||||
InfoLogger.info(s"Installing Enso engine $latest.")
|
||||
installEngine(latest)
|
||||
}
|
||||
|
||||
@ -183,6 +190,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
* @param projectPath if provided, the REPL is run in context of that project
|
||||
* @param versionOverride if provided, overrides the default engine version
|
||||
* that would have been used
|
||||
* @param logLevel log level for the engine
|
||||
* @param useSystemJVM if set, forces to use the default configured JVM,
|
||||
* instead of the JVM associated with the engine version
|
||||
* @param jvmOpts additional options to pass to the launched JVM
|
||||
@ -192,13 +200,16 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
def runRepl(
|
||||
projectPath: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
logLevel: LogLevel,
|
||||
useSystemJVM: Boolean,
|
||||
jvmOpts: Seq[(String, String)],
|
||||
additionalArguments: Seq[String]
|
||||
): Int = {
|
||||
val exitCode = runner
|
||||
.withCommand(
|
||||
runner.repl(projectPath, versionOverride, additionalArguments).get,
|
||||
runner
|
||||
.repl(projectPath, versionOverride, logLevel, additionalArguments)
|
||||
.get,
|
||||
JVMSettings(useSystemJVM, jvmOpts)
|
||||
) { command =>
|
||||
command.run().get
|
||||
@ -218,6 +229,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
* @param path specifies what to run
|
||||
* @param versionOverride if provided, overrides the default engine version
|
||||
* that would have been used
|
||||
* @param logLevel log level for the engine
|
||||
* @param useSystemJVM if set, forces to use the default configured JVM,
|
||||
* instead of the JVM associated with the engine version
|
||||
* @param jvmOpts additional options to pass to the launched JVM
|
||||
@ -227,13 +239,14 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
def runRun(
|
||||
path: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
logLevel: LogLevel,
|
||||
useSystemJVM: Boolean,
|
||||
jvmOpts: Seq[(String, String)],
|
||||
additionalArguments: Seq[String]
|
||||
): Int = {
|
||||
val exitCode = runner
|
||||
.withCommand(
|
||||
runner.run(path, versionOverride, additionalArguments).get,
|
||||
runner.run(path, versionOverride, logLevel, additionalArguments).get,
|
||||
JVMSettings(useSystemJVM, jvmOpts)
|
||||
) { command =>
|
||||
command.run().get
|
||||
@ -250,6 +263,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
* @param options configuration required by the language server
|
||||
* @param versionOverride if provided, overrides the default engine version
|
||||
* that would have been used
|
||||
* @param logLevel log level for the language server
|
||||
* @param useSystemJVM if set, forces to use the default configured JVM,
|
||||
* instead of the JVM associated with the engine version
|
||||
* @param jvmOpts additional options to pass to the launched JVM
|
||||
@ -259,6 +273,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
def runLanguageServer(
|
||||
options: LanguageServerOptions,
|
||||
versionOverride: Option[SemVer],
|
||||
logLevel: LogLevel,
|
||||
useSystemJVM: Boolean,
|
||||
jvmOpts: Seq[(String, String)],
|
||||
additionalArguments: Seq[String]
|
||||
@ -266,7 +281,12 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
val exitCode = runner
|
||||
.withCommand(
|
||||
runner
|
||||
.languageServer(options, versionOverride, additionalArguments)
|
||||
.languageServer(
|
||||
options,
|
||||
versionOverride,
|
||||
logLevel,
|
||||
additionalArguments
|
||||
)
|
||||
.get,
|
||||
JVMSettings(useSystemJVM, jvmOpts)
|
||||
) { command =>
|
||||
@ -286,13 +306,13 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
def updateConfig(key: String, value: String): Int = {
|
||||
if (value.isEmpty) {
|
||||
configurationManager.removeFromConfig(key)
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"""Key `$key` removed from the global configuration file """ +
|
||||
s"(${configurationManager.configLocation.toAbsolutePath})."
|
||||
)
|
||||
} else {
|
||||
configurationManager.updateConfigRaw(key, Json.fromString(value))
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"""Key `$key` set to "$value" in the global configuration file """ +
|
||||
s"(${configurationManager.configLocation.toAbsolutePath})."
|
||||
)
|
||||
@ -312,7 +332,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
println(value)
|
||||
0
|
||||
case None =>
|
||||
Logger.warn(s"Key $key is not set in the global config.")
|
||||
logger.warn(s"Key $key is not set in the global config.")
|
||||
1
|
||||
}
|
||||
}
|
||||
@ -327,12 +347,12 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
|
||||
defaultVersion match {
|
||||
case DefaultVersion.LatestInstalled =>
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"Default Enso version set to the latest installed version, " +
|
||||
s"currently ${configurationManager.defaultVersion}."
|
||||
)
|
||||
case DefaultVersion.Exact(version) =>
|
||||
Logger.info(s"Default Enso version set to $version.")
|
||||
InfoLogger.info(s"Default Enso version set to $version.")
|
||||
}
|
||||
|
||||
0
|
||||
@ -441,14 +461,14 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
val targetVersion = version.getOrElse(upgrader.latestVersion().get)
|
||||
val isManuallyRequested = version.isDefined
|
||||
if (targetVersion == CurrentVersion.version) {
|
||||
Logger.info("Already up-to-date.")
|
||||
InfoLogger.info("Already up-to-date.")
|
||||
0
|
||||
} else if (targetVersion < CurrentVersion.version && !isManuallyRequested) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"The latest available version is $targetVersion, but you are " +
|
||||
s"running ${CurrentVersion.version} which is more recent."
|
||||
)
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"If you really want to downgrade, please run " +
|
||||
s"`enso upgrade $targetVersion`."
|
||||
)
|
||||
@ -471,11 +491,11 @@ object Launcher {
|
||||
*/
|
||||
def ensurePortable(): Unit = {
|
||||
if (!DistributionManager.isRunningPortable) {
|
||||
Logger.error(
|
||||
Logger[Launcher].error(
|
||||
"`--ensure-portable` is set, but the launcher is not running in " +
|
||||
"portable mode. Terminating."
|
||||
)
|
||||
sys.exit(1)
|
||||
Main.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,94 +0,0 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.io.PrintStream
|
||||
|
||||
/**
|
||||
* This is a temporary object that should be at some point replaced with the
|
||||
* actual logging service.
|
||||
*
|
||||
* TODO [RW] this should be replaced with the proper logging service once it
|
||||
* is implemented in #1031
|
||||
*/
|
||||
object Logger {
|
||||
|
||||
// TODO [RW] this stream closure is not very efficient, but it allows the
|
||||
// Logger to respect stream redirection from Console.withErr. Ideally, the
|
||||
// new logging service should allow some way to capture logs for use in the
|
||||
// tests.
|
||||
private case class Level(name: String, level: Int, stream: () => PrintStream)
|
||||
private val Debug = Level("debug", 1, () => Console.err)
|
||||
private val Info = Level("info", 2, () => Console.out)
|
||||
private val Warning = Level("warn", 3, () => Console.err)
|
||||
private val Error = Level("error", 4, () => Console.err)
|
||||
|
||||
private var logLevel = Info
|
||||
private def log(level: Level, msg: => String): Unit =
|
||||
if (level.level >= logLevel.level) {
|
||||
val stream = level.stream()
|
||||
stream.println(s"[${level.name}] $msg")
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug level message.
|
||||
*/
|
||||
def debug(msg: => String): Unit = log(Debug, msg)
|
||||
|
||||
/**
|
||||
* Logs a debug level message and attaches a stack trace.
|
||||
*/
|
||||
def debug(msg: => String, throwable: => Throwable): Unit = {
|
||||
log(Debug, msg)
|
||||
trace(throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an info level message.
|
||||
*/
|
||||
def info(msg: => String): Unit = log(Info, msg)
|
||||
|
||||
/**
|
||||
* Logs a warning level message.
|
||||
*/
|
||||
def warn(msg: => String): Unit = log(Warning, msg)
|
||||
|
||||
/**
|
||||
* Logs an error level message.
|
||||
*/
|
||||
def error(msg: => String): Unit = log(Error, msg)
|
||||
|
||||
/**
|
||||
* Logs an error level message and attaches an optional, debug-level stack
|
||||
* trace.
|
||||
*/
|
||||
def error(msg: => String, throwable: => Throwable): Unit = {
|
||||
log(Error, msg)
|
||||
trace(throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a stack trace of an exception.
|
||||
*/
|
||||
def trace(throwable: => Throwable): Unit =
|
||||
if (Debug.level >= logLevel.level)
|
||||
throwable.printStackTrace()
|
||||
|
||||
/**
|
||||
* Runs the provided action with a log level that will allow only for errors
|
||||
* and returns its result.
|
||||
*
|
||||
* Warning: This function is not thread safe, so using it in tests that are
|
||||
* run in parallel without forking may lead to an inconsistency in logging.
|
||||
* This is just a *temporary* solution until a fully-fledged logging service
|
||||
* is developed #1031.
|
||||
*/
|
||||
def suppressWarnings[R](action: => R): R = {
|
||||
val oldLevel = logLevel
|
||||
try {
|
||||
logLevel = Error
|
||||
action
|
||||
} finally {
|
||||
logLevel = oldLevel
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import io.circe.{Decoder, DecodingFailure}
|
||||
|
||||
/**
|
||||
@ -24,6 +25,8 @@ sealed trait OS {
|
||||
*/
|
||||
object OS {
|
||||
|
||||
private val logger = Logger[OS.type]
|
||||
|
||||
/**
|
||||
* Represents the Linux operating system.
|
||||
*/
|
||||
@ -89,12 +92,12 @@ object OS {
|
||||
case Some(value) =>
|
||||
knownOS.find(value.toLowerCase == _.configName) match {
|
||||
case Some(overriden) =>
|
||||
Logger.debug(
|
||||
logger.debug(
|
||||
s"OS overriden by $ENSO_OPERATING_SYSTEM to $overriden."
|
||||
)
|
||||
return overriden
|
||||
case None =>
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"$ENSO_OPERATING_SYSTEM is set to an unknown value `$value`, " +
|
||||
s"ignoring. Possible values are $knownOSPossibleValuesString."
|
||||
)
|
||||
@ -107,7 +110,7 @@ object OS {
|
||||
if (possibleOS.length == 1) {
|
||||
possibleOS.head
|
||||
} else {
|
||||
Logger.error(
|
||||
logger.error(
|
||||
s"Could not determine a supported operating system. Please make sure " +
|
||||
s"the OS you are running is supported. You can try to manually " +
|
||||
s"override the operating system detection by setting an environment " +
|
||||
|
@ -3,9 +3,10 @@ package org.enso.launcher.archive
|
||||
import java.io.BufferedInputStream
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.apache.commons.compress.archivers.{
|
||||
ArchiveEntry => ApacheArchiveEntry,
|
||||
ArchiveInputStream
|
||||
ArchiveInputStream,
|
||||
ArchiveEntry => ApacheArchiveEntry
|
||||
}
|
||||
import org.apache.commons.compress.archivers.tar.{
|
||||
TarArchiveEntry,
|
||||
@ -20,7 +21,7 @@ import org.apache.commons.io.IOUtils
|
||||
import org.enso.cli.{TaskProgress, TaskProgressImplementation}
|
||||
import org.enso.launcher.archive.internal.{ArchiveIterator, BaseRenamer}
|
||||
import org.enso.launcher.internal.ReadProgress
|
||||
import org.enso.launcher.{FileSystem, Logger, OS}
|
||||
import org.enso.launcher.{FileSystem, OS}
|
||||
|
||||
import scala.util.{Try, Using}
|
||||
|
||||
@ -32,6 +33,8 @@ import scala.util.{Try, Using}
|
||||
*/
|
||||
object Archive {
|
||||
|
||||
private val logger = Logger[Archive.type]
|
||||
|
||||
/**
|
||||
* Extracts the archive at `archivePath` to `destinationDirectory`.
|
||||
*
|
||||
@ -163,7 +166,7 @@ object Archive {
|
||||
val taskProgress = new TaskProgressImplementation[Unit]
|
||||
|
||||
def runExtraction(): Unit = {
|
||||
Logger.debug(s"Opening `$archivePath`.")
|
||||
logger.debug(s"Opening `$archivePath`.")
|
||||
var missingPermissions: Int = 0
|
||||
|
||||
val result = withOpenArchive(archivePath, format) { (archive, progress) =>
|
||||
@ -222,7 +225,7 @@ object Archive {
|
||||
}
|
||||
|
||||
if (missingPermissions > 0) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"Could not find permissions for $missingPermissions files in " +
|
||||
s"archive `$archivePath`, some files may not have been marked as " +
|
||||
s"executable."
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import akka.http.scaladsl.model.{IllegalUriException, Uri}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.arguments.{Argument, OptsParseError}
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
object Arguments {
|
||||
|
||||
@ -14,4 +16,20 @@ object Arguments {
|
||||
OptsParseError(s"`$string` is not a valid semantic version string.")
|
||||
)
|
||||
|
||||
implicit val uriArgument: Argument[Uri] = (string: String) =>
|
||||
try {
|
||||
Right(Uri(string))
|
||||
} catch {
|
||||
case error: IllegalUriException =>
|
||||
Left(OptsParseError(s"`$string` is not a valid Uri: $error."))
|
||||
}
|
||||
|
||||
implicit val logLevelArgument: Argument[LogLevel] = (string: String) => {
|
||||
val provided = string.toLowerCase
|
||||
LogLevel.allLevels
|
||||
.find(_.toString.toLowerCase == provided)
|
||||
.toRight(
|
||||
OptsParseError(s"`$string` is not a valid log level.")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import org.enso.cli.arguments.{Argument, OptsParseError}
|
||||
import org.enso.launcher.cli.GlobalCLIOptions.InternalOptions
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
/**
|
||||
* Gathers settings set by the global CLI options.
|
||||
*
|
||||
@ -10,17 +15,50 @@ package org.enso.launcher.cli
|
||||
* printed
|
||||
* @param useJSON specifies if output should be in JSON format, if it is
|
||||
* supported (currently only the version command supports JSON)
|
||||
* @param colorMode specifies if console output should contain colors
|
||||
* @param internalOptions options that are remembered to pass them to launcher
|
||||
* child processes
|
||||
*/
|
||||
case class GlobalCLIOptions(
|
||||
autoConfirm: Boolean,
|
||||
hideProgress: Boolean,
|
||||
useJSON: Boolean
|
||||
useJSON: Boolean,
|
||||
colorMode: ColorMode,
|
||||
internalOptions: InternalOptions = InternalOptions(None, None)
|
||||
)
|
||||
|
||||
object GlobalCLIOptions {
|
||||
val HIDE_PROGRESS = "hide-progress"
|
||||
val AUTO_CONFIRM = "auto-confirm"
|
||||
val USE_JSON = "json"
|
||||
val COLOR_MODE = "color"
|
||||
|
||||
/**
|
||||
* Internal options that are remembered to pass them to launcher child
|
||||
* processes.
|
||||
*/
|
||||
case class InternalOptions(
|
||||
launcherLogLevel: Option[LogLevel],
|
||||
loggerConnectUri: Option[Uri]
|
||||
) {
|
||||
|
||||
/**
|
||||
* Creates command line options that can be passed to a launcher process to
|
||||
* set the same options.
|
||||
*/
|
||||
def toOptions: Seq[String] = {
|
||||
val level = launcherLogLevel
|
||||
.map(level => Seq(s"--$LOG_LEVEL", level.toString))
|
||||
.getOrElse(Seq())
|
||||
val uri = loggerConnectUri
|
||||
.map(uri => Seq(s"--$CONNECT_LOGGER", uri.toString))
|
||||
.getOrElse(Seq())
|
||||
level ++ uri
|
||||
}
|
||||
}
|
||||
|
||||
val LOG_LEVEL = "launcher-log-level"
|
||||
val CONNECT_LOGGER = "internal-connect-logger"
|
||||
|
||||
/**
|
||||
* Converts the [[GlobalCLIOptions]] to a sequence of arguments that can be
|
||||
@ -31,6 +69,62 @@ object GlobalCLIOptions {
|
||||
val hideProgress =
|
||||
if (config.hideProgress) Seq(s"--$HIDE_PROGRESS") else Seq()
|
||||
val useJSON = if (config.useJSON) Seq(s"--$USE_JSON") else Seq()
|
||||
autoConfirm ++ hideProgress ++ useJSON
|
||||
autoConfirm ++ hideProgress ++ useJSON ++
|
||||
ColorMode.toOptions(config.colorMode) ++ config.internalOptions.toOptions
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes possible modes of color display in console output.
|
||||
*/
|
||||
sealed trait ColorMode
|
||||
object ColorMode {
|
||||
|
||||
/**
|
||||
* Never use color escape sequences in the output.
|
||||
*/
|
||||
case object Never extends ColorMode
|
||||
|
||||
/**
|
||||
* Enable color output if it seems to be supported.
|
||||
*/
|
||||
case object Auto extends ColorMode
|
||||
|
||||
/**
|
||||
* Always use escape sequences in the output, even if the program thinks they
|
||||
* are unsupported.
|
||||
*
|
||||
* May be useful if output is piped to other programs that know how to handle
|
||||
* the escape sequences.
|
||||
*/
|
||||
case object Always extends ColorMode
|
||||
|
||||
/**
|
||||
* [[Argument]] instance used to parse [[ColorMode]] from CLI.
|
||||
*/
|
||||
implicit val argument: Argument[ColorMode] = {
|
||||
case "never" => Right(Never)
|
||||
case "no" => Right(Never)
|
||||
case "auto" => Right(Auto)
|
||||
case "always" => Right(Always)
|
||||
case "yes" => Right(Always)
|
||||
case other =>
|
||||
OptsParseError.left(
|
||||
s"Unknown color mode value `$other`. Supported values are: " +
|
||||
s"never | no | auto | always | yes."
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates command line options that can be passed to a launcher process to
|
||||
* inherit our color mode.
|
||||
*/
|
||||
def toOptions(colorMode: ColorMode): Seq[String] = {
|
||||
val name = colorMode match {
|
||||
case Never => "never"
|
||||
case Auto => "auto"
|
||||
case Always => "always"
|
||||
}
|
||||
Seq(s"--${GlobalCLIOptions.COLOR_MODE}", name)
|
||||
}
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ object InternalOpts {
|
||||
LauncherUpgrader
|
||||
.default(config, originalExecutablePath = originalPath)
|
||||
.internalContinueUpgrade(version)
|
||||
sys.exit(0)
|
||||
Main.exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package org.enso.launcher.cli
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
@ -19,6 +20,7 @@ import org.enso.launcher.installation.{
|
||||
DistributionManager
|
||||
}
|
||||
import org.enso.launcher.locking.DefaultResourceManager
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
/**
|
||||
* Defines the CLI commands and options for the program.
|
||||
@ -99,12 +101,22 @@ object LauncherApplication {
|
||||
"Advanced option, use carefully.",
|
||||
showInUsage = false
|
||||
)
|
||||
private def versionOverride =
|
||||
private def versionOverride = {
|
||||
Opts.optionalParameter[SemVer](
|
||||
"use-enso-version",
|
||||
"VERSION",
|
||||
"Override the Enso version that would normally be used."
|
||||
)
|
||||
}
|
||||
private def engineLogLevel = {
|
||||
Opts
|
||||
.optionalParameter[LogLevel](
|
||||
"log-level",
|
||||
"(error | warning | info | debug | trace)",
|
||||
"Sets logging verbosity for the engine. Defaults to info."
|
||||
)
|
||||
.withDefault(LogLevel.Info)
|
||||
}
|
||||
|
||||
private def runCommand: Command[Config => Int] =
|
||||
Command(
|
||||
@ -126,19 +138,27 @@ object LauncherApplication {
|
||||
(
|
||||
pathOpt,
|
||||
versionOverride,
|
||||
engineLogLevel,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(path, versionOverride, systemJVMOverride, jvmOpts, additionalArgs) =>
|
||||
(config: Config) =>
|
||||
Launcher(config).runRun(
|
||||
path = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
)
|
||||
(
|
||||
path,
|
||||
versionOverride,
|
||||
engineLogLevel,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) => (config: Config) =>
|
||||
Launcher(config).runRun(
|
||||
path = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs,
|
||||
logLevel = engineLogLevel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,24 +174,30 @@ object LauncherApplication {
|
||||
val path =
|
||||
Opts.parameter[Path]("path", "PATH", "Path to the content root.")
|
||||
val interface =
|
||||
Opts.optionalParameter[String](
|
||||
"interface",
|
||||
"INTERFACE",
|
||||
"Interface for processing all incoming connections. " +
|
||||
"Defaults to `127.0.0.1`."
|
||||
)
|
||||
Opts
|
||||
.optionalParameter[String](
|
||||
"interface",
|
||||
"INTERFACE",
|
||||
"Interface for processing all incoming connections. " +
|
||||
"Defaults to `127.0.0.1`."
|
||||
)
|
||||
.withDefault("127.0.0.1")
|
||||
val rpcPort =
|
||||
Opts.optionalParameter[Int](
|
||||
"rpc-port",
|
||||
"PORT",
|
||||
"RPC port for processing all incoming connections. Defaults to 8080."
|
||||
)
|
||||
Opts
|
||||
.optionalParameter[Int](
|
||||
"rpc-port",
|
||||
"PORT",
|
||||
"RPC port for processing all incoming connections. Defaults to 8080."
|
||||
)
|
||||
.withDefault(8080)
|
||||
val dataPort =
|
||||
Opts.optionalParameter[Int](
|
||||
"data-port",
|
||||
"PORT",
|
||||
"Data port for visualisation protocol. Defaults to 8081."
|
||||
)
|
||||
Opts
|
||||
.optionalParameter[Int](
|
||||
"data-port",
|
||||
"PORT",
|
||||
"Data port for visualisation protocol. Defaults to 8081."
|
||||
)
|
||||
.withDefault(8081)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(
|
||||
rootId,
|
||||
@ -180,6 +206,7 @@ object LauncherApplication {
|
||||
rpcPort,
|
||||
dataPort,
|
||||
versionOverride,
|
||||
engineLogLevel,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
@ -191,6 +218,7 @@ object LauncherApplication {
|
||||
rpcPort,
|
||||
dataPort,
|
||||
versionOverride,
|
||||
engineLogLevel,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
@ -199,14 +227,15 @@ object LauncherApplication {
|
||||
options = LanguageServerOptions(
|
||||
rootId = rootId,
|
||||
path = path,
|
||||
interface = interface.getOrElse("127.0.0.1"),
|
||||
rpcPort = rpcPort.getOrElse(8080),
|
||||
dataPort = dataPort.getOrElse(8081)
|
||||
interface = interface,
|
||||
rpcPort = rpcPort,
|
||||
dataPort = dataPort
|
||||
),
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
additionalArguments = additionalArgs,
|
||||
logLevel = engineLogLevel
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -226,16 +255,30 @@ object LauncherApplication {
|
||||
"project if it is launched from within a directory inside a project."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(path, versionOverride, systemJVMOverride, jvmOpts, additionalArgs) mapN {
|
||||
(path, versionOverride, systemJVMOverride, jvmOpts, additionalArgs) =>
|
||||
(config: Config) =>
|
||||
Launcher(config).runRepl(
|
||||
projectPath = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
)
|
||||
(
|
||||
path,
|
||||
versionOverride,
|
||||
engineLogLevel,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(
|
||||
path,
|
||||
versionOverride,
|
||||
engineLogLevel,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) => (config: Config) =>
|
||||
Launcher(config).runRepl(
|
||||
projectPath = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs,
|
||||
logLevel = engineLogLevel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -453,6 +496,32 @@ object LauncherApplication {
|
||||
"running actions. May be needed if program output is piped.",
|
||||
showInUsage = false
|
||||
)
|
||||
val logLevel = Opts.optionalParameter[LogLevel](
|
||||
GlobalCLIOptions.LOG_LEVEL,
|
||||
"(error | warning | info | debug | trace)",
|
||||
"Sets logging verbosity for the launcher. If not provided, defaults to" +
|
||||
s"${LauncherLogging.defaultLogLevel}."
|
||||
)
|
||||
val connectLogger = Opts
|
||||
.optionalParameter[Uri](
|
||||
GlobalCLIOptions.CONNECT_LOGGER,
|
||||
"URI",
|
||||
"Instead of starting its own logging service, " +
|
||||
"connects to the logging service at the provided URI."
|
||||
)
|
||||
.hidden
|
||||
val colorMode =
|
||||
Opts
|
||||
.aliasedOptionalParameter[ColorMode](
|
||||
GlobalCLIOptions.COLOR_MODE,
|
||||
"colour",
|
||||
"colors"
|
||||
)(
|
||||
"(auto | yes | always | no | never)",
|
||||
"Specifies if colors should be used in the output, defaults to auto."
|
||||
)
|
||||
.withDefault(ColorMode.Auto)
|
||||
|
||||
val internalOpts = InternalOpts.topLevelOptions
|
||||
|
||||
(
|
||||
@ -461,7 +530,10 @@ object LauncherApplication {
|
||||
json,
|
||||
ensurePortable,
|
||||
autoConfirm,
|
||||
hideProgress
|
||||
hideProgress,
|
||||
logLevel,
|
||||
connectLogger,
|
||||
colorMode
|
||||
) mapN {
|
||||
(
|
||||
internalOptsCallback,
|
||||
@ -469,7 +541,10 @@ object LauncherApplication {
|
||||
useJSON,
|
||||
shouldEnsurePortable,
|
||||
autoConfirm,
|
||||
hideProgress
|
||||
hideProgress,
|
||||
logLevel,
|
||||
connectLogger,
|
||||
colorMode
|
||||
) => () =>
|
||||
if (shouldEnsurePortable) {
|
||||
Launcher.ensurePortable()
|
||||
@ -478,10 +553,14 @@ object LauncherApplication {
|
||||
val globalCLIOptions = GlobalCLIOptions(
|
||||
autoConfirm = autoConfirm,
|
||||
hideProgress = hideProgress,
|
||||
useJSON = useJSON
|
||||
useJSON = useJSON,
|
||||
colorMode = colorMode,
|
||||
internalOptions =
|
||||
GlobalCLIOptions.InternalOptions(logLevel, connectLogger)
|
||||
)
|
||||
|
||||
internalOptsCallback(globalCLIOptions)
|
||||
LauncherLogging.setup(logLevel, connectLogger, globalCLIOptions)
|
||||
initializeApp()
|
||||
|
||||
if (version) {
|
||||
|
@ -0,0 +1,243 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.loggingservice.printers.{
|
||||
FileOutputPrinter,
|
||||
Printer,
|
||||
StderrPrinter,
|
||||
StderrPrinterWithColors
|
||||
}
|
||||
import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success}
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.{Await, Future, Promise}
|
||||
|
||||
/**
|
||||
* Manages setting up the logging service within the launcher.
|
||||
*/
|
||||
object LauncherLogging {
|
||||
private val logger = Logger[LauncherLogging.type]
|
||||
|
||||
/**
|
||||
* Default logl level to use if none is provided.
|
||||
*/
|
||||
val defaultLogLevel: LogLevel = LogLevel.Warning
|
||||
|
||||
/**
|
||||
* Sets up launcher's logging service as either a server that gathers other
|
||||
* component's logs or a client that forwards them further.
|
||||
*
|
||||
* Forwarding logs to another server in the launcher is an internal,
|
||||
* development-mode feature that is not designed to be used by end-users
|
||||
* unless they specifically know what they are doing. Redirecting logs to an
|
||||
* external server may result in some important information not being printed
|
||||
* by the launcher, being forwarded instead.
|
||||
*
|
||||
* @param logLevel the log level to use for launcher's logs; does not affect
|
||||
* other component's log level, which has to be set
|
||||
* separately
|
||||
* @param connectToExternalLogger specifies an Uri of an external logging
|
||||
* service that the launcher should forward
|
||||
* its logs to; advanced feature, use with
|
||||
* caution
|
||||
*/
|
||||
def setup(
|
||||
logLevel: Option[LogLevel],
|
||||
connectToExternalLogger: Option[Uri],
|
||||
globalCLIOptions: GlobalCLIOptions
|
||||
): Unit = {
|
||||
val actualLogLevel = logLevel.getOrElse(defaultLogLevel)
|
||||
connectToExternalLogger match {
|
||||
case Some(uri) =>
|
||||
setupLoggingConnection(uri, actualLogLevel)
|
||||
case None =>
|
||||
setupLoggingServer(actualLogLevel, globalCLIOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a fallback logger that just logs to stderr.
|
||||
*
|
||||
* It can be used when the application has failed to parse the CLI options
|
||||
* and does not know which logger to set up.
|
||||
*/
|
||||
def setupFallback(): Unit = {
|
||||
LoggingServiceManager.setup(
|
||||
LoggerMode.Local(Seq(fallbackPrinter)),
|
||||
defaultLogLevel
|
||||
)
|
||||
}
|
||||
|
||||
private def fallbackPrinter = StderrPrinter.create(printExceptions = true)
|
||||
|
||||
private val loggingServiceEndpointPromise = Promise[Option[Uri]]()
|
||||
|
||||
/**
|
||||
* Returns a [[Uri]] of the logging service that launched components can
|
||||
* connect to.
|
||||
*
|
||||
* Points to the local server if it has been set up, or to the endpoint that
|
||||
* the launcher was told to connect to. May be empty if the initialization
|
||||
* failed and local logging is used as a fallback.
|
||||
*
|
||||
* The future is completed once the
|
||||
*/
|
||||
def loggingServiceEndpoint(): Future[Option[Uri]] =
|
||||
loggingServiceEndpointPromise.future
|
||||
|
||||
/**
|
||||
* Returns a printer for outputting the logs to the standard error.
|
||||
*/
|
||||
private def stderrPrinter(
|
||||
globalCLIOptions: GlobalCLIOptions,
|
||||
printExceptions: Boolean
|
||||
): Printer =
|
||||
globalCLIOptions.colorMode match {
|
||||
case ColorMode.Never =>
|
||||
StderrPrinter.create(printExceptions)
|
||||
case ColorMode.Auto =>
|
||||
StderrPrinterWithColors.colorPrinterIfAvailable(printExceptions)
|
||||
case ColorMode.Always =>
|
||||
StderrPrinterWithColors.forceCreate(printExceptions)
|
||||
}
|
||||
|
||||
private def setupLoggingServer(
|
||||
logLevel: LogLevel,
|
||||
globalCLIOptions: GlobalCLIOptions
|
||||
): Unit = {
|
||||
val printExceptionsInStderr =
|
||||
implicitly[Ordering[LogLevel]].compare(logLevel, LogLevel.Debug) >= 0
|
||||
|
||||
/**
|
||||
* Creates a stderr printer and a file printer if a log file can be opened.
|
||||
*
|
||||
* This is a `def` on purpose, as even if the service fails, the printers
|
||||
* are shut down, so the fallback must create new instances.
|
||||
*/
|
||||
def createPrinters() =
|
||||
try {
|
||||
val filePrinter =
|
||||
FileOutputPrinter.create(DistributionManager.paths.logs)
|
||||
Seq(
|
||||
stderrPrinter(globalCLIOptions, printExceptionsInStderr),
|
||||
filePrinter
|
||||
)
|
||||
} catch {
|
||||
case NonFatal(error) =>
|
||||
logger.error(
|
||||
"Failed to initialize the write-to-file logger, " +
|
||||
"falling back to stderr only.",
|
||||
error
|
||||
)
|
||||
Seq(stderrPrinter(globalCLIOptions, printExceptions = true))
|
||||
}
|
||||
|
||||
LoggingServiceManager
|
||||
.setup(LoggerMode.Server(createPrinters()), logLevel)
|
||||
.onComplete {
|
||||
case Failure(exception) =>
|
||||
logger.error(
|
||||
s"Failed to initialize the logging service server: $exception",
|
||||
exception
|
||||
)
|
||||
logger.warn("Falling back to local-only logger.")
|
||||
loggingServiceEndpointPromise.success(None)
|
||||
LoggingServiceManager
|
||||
.setup(
|
||||
LoggerMode.Local(createPrinters()),
|
||||
logLevel
|
||||
)
|
||||
.onComplete {
|
||||
case Failure(fallbackException) =>
|
||||
System.err.println(
|
||||
s"Failed to initialize the fallback logger: " +
|
||||
s"$fallbackException"
|
||||
)
|
||||
fallbackException.printStackTrace()
|
||||
case Success(_) =>
|
||||
}
|
||||
case Success(serverBinding) =>
|
||||
val uri = serverBinding.toUri()
|
||||
loggingServiceEndpointPromise.success(Some(uri))
|
||||
logger.trace(
|
||||
s"Logging service has been set-up and is listening at `$uri`."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects this launcher to an external logging service.
|
||||
*
|
||||
* Currently, this is an internal function used mostly for testing purposes.
|
||||
* It is not a user-facing API.
|
||||
*/
|
||||
private def setupLoggingConnection(uri: Uri, logLevel: LogLevel): Unit = {
|
||||
LoggingServiceManager
|
||||
.setup(
|
||||
LoggerMode.Client(uri),
|
||||
logLevel
|
||||
)
|
||||
.map(_ => true)
|
||||
.recoverWith { _ =>
|
||||
LoggingServiceManager
|
||||
.setup(
|
||||
LoggerMode.Local(Seq(fallbackPrinter)),
|
||||
logLevel
|
||||
)
|
||||
.map(_ => false)
|
||||
}
|
||||
.onComplete {
|
||||
case Failure(exception) =>
|
||||
System.err.println(s"Failed to initialize the logger: $exception")
|
||||
exception.printStackTrace()
|
||||
loggingServiceEndpointPromise.success(None)
|
||||
case Success(connected) =>
|
||||
if (connected) {
|
||||
loggingServiceEndpointPromise.success(Some(uri))
|
||||
System.err.println(
|
||||
s"Log messages from this launcher are forwarded to `$uri`."
|
||||
)
|
||||
} else {
|
||||
loggingServiceEndpointPromise.success(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the logging service has been set-up.
|
||||
*
|
||||
* Due to limitations of how the logging service is implemented, it can only
|
||||
* be terminated after it has been set up.
|
||||
*/
|
||||
def waitForSetup(): Unit = {
|
||||
Await.ready(loggingServiceEndpointPromise.future, 5.seconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off the main logging service, falling back to just a stderr backend.
|
||||
*
|
||||
* This method should be called as part of uninstalling the distribution. The
|
||||
* server can be safely shutdown as during uninstallation no other components
|
||||
* should be running.
|
||||
*
|
||||
* This is necessary on Windows to ensure that the logs file is closed, so
|
||||
* that the log directory can be removed.
|
||||
*/
|
||||
def prepareForUninstall(globalCLIOptions: GlobalCLIOptions): Unit = {
|
||||
waitForSetup()
|
||||
LoggingServiceManager.replaceWithFallback(printers =
|
||||
Seq(stderrPrinter(globalCLIOptions, printExceptions = true))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the logging service gracefully.
|
||||
*/
|
||||
def tearDown(): Unit =
|
||||
LoggingServiceManager.tearDown()
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.locking.DefaultResourceManager
|
||||
import org.enso.launcher.upgrade.LauncherUpgrader
|
||||
|
||||
@ -18,12 +18,18 @@ object Main {
|
||||
private def runAppHandlingParseErrors(args: Array[String]): Int =
|
||||
LauncherApplication.application.run(args) match {
|
||||
case Left(errors) =>
|
||||
LauncherLogging.setupFallback()
|
||||
CLIOutput.println(errors.mkString("\n"))
|
||||
1
|
||||
case Right(exitCode) =>
|
||||
exitCode
|
||||
}
|
||||
|
||||
private val logger = Logger[Main.type]
|
||||
|
||||
/**
|
||||
* Entry point of the application.
|
||||
*/
|
||||
def main(args: Array[String]): Unit = {
|
||||
setup()
|
||||
val exitCode =
|
||||
@ -33,10 +39,26 @@ object Main {
|
||||
}
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
Logger.error(s"A fatal error has occurred: $e", e)
|
||||
logger.error(s"A fatal error has occurred: $e", e)
|
||||
1
|
||||
}
|
||||
|
||||
exit(exitCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Exits the program in a safe way.
|
||||
*
|
||||
* This should be used ofer `sys.exit` to ensure that all services are
|
||||
* terminated gracefully and locks are released quickly (as the OS cleanup
|
||||
* may take a longer while). The only exception is for functions in the
|
||||
* [[InternalOpts]], because they may need to terminate the program as
|
||||
* quickly as possible.
|
||||
*
|
||||
* @param exitCode exit code to return
|
||||
*/
|
||||
def exit(exitCode: Int): Nothing = {
|
||||
LauncherLogging.tearDown()
|
||||
DefaultResourceManager.releaseMainLock()
|
||||
sys.exit(exitCode)
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.{Files, Path, StandardOpenOption}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
@ -20,7 +21,7 @@ import org.enso.launcher.releases.runtime.{
|
||||
RuntimeReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.releases.{EnsoRepository, ReleaseProvider}
|
||||
import org.enso.launcher.{FileSystem, Logger}
|
||||
import org.enso.launcher.{FileSystem, InfoLogger}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Try, Using}
|
||||
@ -46,6 +47,7 @@ class ComponentsManager(
|
||||
runtimeReleaseProvider: RuntimeReleaseProvider
|
||||
) {
|
||||
private val showProgress = !cliOptions.hideProgress
|
||||
private val logger = Logger[ComponentsManager]
|
||||
|
||||
/**
|
||||
* Tries to find runtime for the provided engine.
|
||||
@ -183,7 +185,7 @@ class ComponentsManager(
|
||||
case Some(found) => found
|
||||
case None =>
|
||||
def complainAndAsk(): Boolean = {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"Runtime ${engine.manifest.runtimeVersion} required for $engine " +
|
||||
s"is missing."
|
||||
)
|
||||
@ -231,7 +233,7 @@ class ComponentsManager(
|
||||
getEngine(version)
|
||||
.map { engine =>
|
||||
if (engine.isMarkedBroken) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"Running an engine release ($version) that is marked as broken. " +
|
||||
s"Please consider upgrading to a stable release."
|
||||
)
|
||||
@ -270,7 +272,7 @@ class ComponentsManager(
|
||||
case Some(found) => found
|
||||
case None =>
|
||||
def complainAndAsk(): Boolean = {
|
||||
Logger.warn(s"Engine $version is missing.")
|
||||
logger.warn(s"Engine $version is missing.")
|
||||
cliOptions.autoConfirm || CLIOutput.askConfirmation(
|
||||
"Do you want to install the missing engine?",
|
||||
yesDefault = true
|
||||
@ -284,7 +286,7 @@ class ComponentsManager(
|
||||
) {
|
||||
findEngine(version) match {
|
||||
case Some(engine) =>
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
"The engine has already been installed by a different " +
|
||||
"process."
|
||||
)
|
||||
@ -335,7 +337,7 @@ class ComponentsManager(
|
||||
): Seq[A] =
|
||||
result match {
|
||||
case (path, Failure(exception)) =>
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"$name at $path has been skipped due to the following error: " +
|
||||
s"$exception"
|
||||
)
|
||||
@ -359,12 +361,12 @@ class ComponentsManager(
|
||||
(Resource.Engine(version), LockType.Exclusive)
|
||||
) {
|
||||
val engine = getEngine(version).getOrElse {
|
||||
Logger.warn(s"Enso Engine $version is not installed.")
|
||||
logger.warn(s"Enso Engine $version is not installed.")
|
||||
throw ComponentMissingError(s"Enso Engine $version is not installed.")
|
||||
}
|
||||
|
||||
safelyRemoveComponent(engine.path)
|
||||
Logger.info(s"Uninstalled $engine.")
|
||||
InfoLogger.info(s"Uninstalled $engine.")
|
||||
cleanupRuntimes()
|
||||
}
|
||||
|
||||
@ -393,14 +395,14 @@ class ComponentsManager(
|
||||
}
|
||||
if (engineRelease.isBroken) {
|
||||
if (cliOptions.autoConfirm) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"The engine release $version is marked as broken and it should " +
|
||||
s"not be used. Since `auto-confirm` is set, the installation will " +
|
||||
s"continue, but you may want to reconsider changing versions to a " +
|
||||
s"stable release."
|
||||
)
|
||||
} else {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"The engine release $version is marked as broken and it should " +
|
||||
s"not be used."
|
||||
)
|
||||
@ -417,9 +419,9 @@ class ComponentsManager(
|
||||
}
|
||||
}
|
||||
FileSystem.withTemporaryDirectory("enso-install") { directory =>
|
||||
Logger.debug(s"Downloading packages to $directory")
|
||||
logger.debug(s"Downloading packages to $directory")
|
||||
val enginePackage = directory / engineRelease.packageFileName
|
||||
Logger.info(s"Downloading ${enginePackage.getFileName}.")
|
||||
InfoLogger.info(s"Downloading ${enginePackage.getFileName}.")
|
||||
engineRelease
|
||||
.downloadPackage(enginePackage)
|
||||
.waitForResult(showProgress)
|
||||
@ -428,7 +430,7 @@ class ComponentsManager(
|
||||
val engineDirectoryName =
|
||||
engineDirectoryNameForVersion(engineRelease.version)
|
||||
|
||||
Logger.info(s"Extracting the engine.")
|
||||
InfoLogger.info(s"Extracting the engine.")
|
||||
Archive
|
||||
.extractArchive(
|
||||
enginePackage,
|
||||
@ -503,7 +505,7 @@ class ComponentsManager(
|
||||
distributionManager.paths.engines / engineDirectoryName
|
||||
FileSystem.atomicMove(engineTemporaryPath, enginePath)
|
||||
val engine = getEngine(version).getOrElse {
|
||||
Logger.error(
|
||||
logger.error(
|
||||
"fatal: Could not load the installed engine." +
|
||||
"Reverting the installation."
|
||||
)
|
||||
@ -517,7 +519,7 @@ class ComponentsManager(
|
||||
)
|
||||
}
|
||||
|
||||
Logger.info(s"Installed $engine.")
|
||||
InfoLogger.info(s"Installed $engine.")
|
||||
engine
|
||||
}
|
||||
|
||||
@ -604,13 +606,13 @@ class ComponentsManager(
|
||||
case Some(graalVersion) =>
|
||||
Some(RuntimeVersion(graalVersion, javaVersionString))
|
||||
case None =>
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"Invalid runtime version string `$graalVersionString`."
|
||||
)
|
||||
None
|
||||
}
|
||||
case _ =>
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"Unrecognized runtime name `$name`."
|
||||
)
|
||||
None
|
||||
@ -677,7 +679,7 @@ class ComponentsManager(
|
||||
FileSystem.withTemporaryDirectory("enso-install-runtime") { directory =>
|
||||
val runtimePackage =
|
||||
directory / runtimeReleaseProvider.packageFileName(runtimeVersion)
|
||||
Logger.info(s"Downloading ${runtimePackage.getFileName}.")
|
||||
InfoLogger.info(s"Downloading ${runtimePackage.getFileName}.")
|
||||
runtimeReleaseProvider
|
||||
.downloadPackage(runtimeVersion, runtimePackage)
|
||||
.waitForResult(showProgress)
|
||||
@ -685,7 +687,7 @@ class ComponentsManager(
|
||||
|
||||
val runtimeDirectoryName = graalDirectoryForVersion(runtimeVersion)
|
||||
|
||||
Logger.info(s"Extracting the runtime.")
|
||||
InfoLogger.info(s"Extracting the runtime.")
|
||||
Archive
|
||||
.extractArchive(
|
||||
runtimePackage,
|
||||
@ -723,7 +725,7 @@ class ComponentsManager(
|
||||
)
|
||||
}
|
||||
|
||||
Logger.info(s"Installed $runtime.")
|
||||
InfoLogger.info(s"Installed $runtime.")
|
||||
runtime
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
@ -746,7 +748,7 @@ class ComponentsManager(
|
||||
private def cleanupRuntimes(): Unit = {
|
||||
for (runtime <- listInstalledRuntimes()) {
|
||||
if (findEnginesUsingRuntime(runtime).isEmpty) {
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"Removing $runtime, because it is not used by any installed Enso " +
|
||||
s"versions."
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
import com.typesafe.scalalogging.Logger
|
||||
|
||||
import scala.sys.process.Process
|
||||
import scala.util.{Failure, Try}
|
||||
@ -12,6 +12,7 @@ import scala.util.{Failure, Try}
|
||||
* @param extraEnv environment variables that should be overridden
|
||||
*/
|
||||
case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
|
||||
private val logger = Logger[Command]
|
||||
|
||||
/**
|
||||
* Runs the command and returns its exit code.
|
||||
@ -21,7 +22,7 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
|
||||
*/
|
||||
def run(): Try[Int] =
|
||||
wrapError {
|
||||
Logger.debug(s"Executing $toString")
|
||||
logger.debug(s"Executing $toString")
|
||||
val processBuilder = new java.lang.ProcessBuilder(command: _*)
|
||||
for ((key, value) <- extraEnv) {
|
||||
processBuilder.environment().put(key, value)
|
||||
@ -41,7 +42,7 @@ case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
|
||||
*/
|
||||
def captureOutput(): Try[String] =
|
||||
wrapError {
|
||||
Logger.debug(s"Executing $toString")
|
||||
logger.debug(s"Executing $toString")
|
||||
val processBuilder = Process(command, None, extraEnv: _*)
|
||||
processBuilder.!!
|
||||
}
|
||||
|
@ -7,5 +7,11 @@ import nl.gn0s1s.bump.SemVer
|
||||
*
|
||||
* @param version 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, runnerArguments: Seq[String])
|
||||
case class RunSettings(
|
||||
version: SemVer,
|
||||
runnerArguments: Seq[String],
|
||||
connectLoggerIfAvailable: Boolean
|
||||
)
|
||||
|
@ -2,7 +2,10 @@ package org.enso.launcher.components.runner
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.Environment
|
||||
import org.enso.launcher.components.{
|
||||
ComponentsManager,
|
||||
Engine,
|
||||
@ -11,8 +14,10 @@ import org.enso.launcher.components.{
|
||||
}
|
||||
import org.enso.launcher.config.GlobalConfigurationManager
|
||||
import org.enso.launcher.project.ProjectManager
|
||||
import org.enso.launcher.{Environment, Logger}
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
import scala.concurrent.duration.DurationInt
|
||||
import scala.concurrent.{Await, Future, TimeoutException}
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
@ -24,7 +29,8 @@ class Runner(
|
||||
projectManager: ProjectManager,
|
||||
configurationManager: GlobalConfigurationManager,
|
||||
componentsManager: ComponentsManager,
|
||||
environment: Environment
|
||||
environment: Environment,
|
||||
loggerConnection: Future[Option[Uri]]
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -55,7 +61,7 @@ class Runner(
|
||||
"--new-project-name",
|
||||
name
|
||||
) ++ authorNameOption ++ authorEmailOption ++ additionalArguments
|
||||
RunSettings(version, arguments)
|
||||
RunSettings(version, arguments, connectLoggerIfAvailable = false)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,6 +72,7 @@ class Runner(
|
||||
def repl(
|
||||
projectPath: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
logLevel: LogLevel,
|
||||
additionalArguments: Seq[String]
|
||||
): Try[RunSettings] =
|
||||
Try {
|
||||
@ -90,7 +97,12 @@ class Runner(
|
||||
case None =>
|
||||
Seq("--repl")
|
||||
}
|
||||
RunSettings(version, arguments ++ additionalArguments)
|
||||
RunSettings(
|
||||
version,
|
||||
arguments ++ Seq("--log-level", logLevel.toString)
|
||||
++ additionalArguments,
|
||||
connectLoggerIfAvailable = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,6 +113,7 @@ class Runner(
|
||||
def run(
|
||||
path: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
logLevel: LogLevel,
|
||||
additionalArguments: Seq[String]
|
||||
): Try[RunSettings] =
|
||||
Try {
|
||||
@ -145,7 +158,12 @@ class Runner(
|
||||
case None =>
|
||||
Seq("--run", actualPath.toString)
|
||||
}
|
||||
RunSettings(version, arguments ++ additionalArguments)
|
||||
RunSettings(
|
||||
version,
|
||||
arguments ++ Seq("--log-level", logLevel.toString)
|
||||
++ additionalArguments,
|
||||
connectLoggerIfAvailable = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,6 +174,7 @@ class Runner(
|
||||
def languageServer(
|
||||
options: LanguageServerOptions,
|
||||
versionOverride: Option[SemVer],
|
||||
logLevel: LogLevel,
|
||||
additionalArguments: Seq[String]
|
||||
): Try[RunSettings] =
|
||||
Try {
|
||||
@ -172,9 +191,17 @@ class Runner(
|
||||
"--rpc-port",
|
||||
options.rpcPort.toString,
|
||||
"--data-port",
|
||||
options.dataPort.toString
|
||||
options.dataPort.toString,
|
||||
"--log-level",
|
||||
logLevel.toString
|
||||
)
|
||||
RunSettings(
|
||||
version,
|
||||
arguments ++ additionalArguments,
|
||||
// TODO [RW] set to true when language server gets logging support
|
||||
// (#1144)
|
||||
connectLoggerIfAvailable = false
|
||||
)
|
||||
RunSettings(version, arguments ++ additionalArguments)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -206,7 +233,10 @@ class Runner(
|
||||
case None => WhichEngine.Default
|
||||
}
|
||||
|
||||
(RunSettings(version, arguments), whichEngine)
|
||||
(
|
||||
RunSettings(version, arguments, connectLoggerIfAvailable = false),
|
||||
whichEngine
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,7 +259,7 @@ class Runner(
|
||||
def prepareAndRunCommand(engine: Engine, javaCommand: JavaCommand): R = {
|
||||
val jvmOptsFromEnvironment = environment.getEnvVar(JVM_OPTIONS_ENV_VAR)
|
||||
jvmOptsFromEnvironment.foreach { opts =>
|
||||
Logger.debug(
|
||||
Logger[Runner].debug(
|
||||
s"Picking up additional JVM options ($opts) from the " +
|
||||
s"$JVM_OPTIONS_ENV_VAR environment variable."
|
||||
)
|
||||
@ -255,8 +285,13 @@ class Runner(
|
||||
manifestOptions ++ environmentOptions ++ commandLineOptions ++
|
||||
Seq("-jar", runnerJar)
|
||||
|
||||
val loggingConnectionArguments =
|
||||
if (runSettings.connectLoggerIfAvailable)
|
||||
forceLoggerConnectionArguments()
|
||||
else Seq()
|
||||
|
||||
val command = Seq(javaCommand.executableName) ++
|
||||
jvmArguments ++ runSettings.runnerArguments
|
||||
jvmArguments ++ loggingConnectionArguments ++ runSettings.runnerArguments
|
||||
|
||||
val extraEnvironmentOverrides =
|
||||
javaCommand.javaHomeOverride.map("JAVA_HOME" -> _).toSeq
|
||||
@ -302,4 +337,37 @@ class Runner(
|
||||
javaHomeOverride =
|
||||
Some(runtime.javaHome.toAbsolutePath.normalize.toString)
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns arguments that should be added to a launched component to connect
|
||||
* it to launcher's logging service.
|
||||
*
|
||||
* It waits until the logging service has been set up and should be called as
|
||||
* late as possible (for example after installing any required components) to
|
||||
* avoid blocking other actions by the logging service setup.
|
||||
*
|
||||
* If the logging service is not available in 3 seconds after calling this
|
||||
* method, it assumes that it failed to boot and returns arguments that will
|
||||
* cause the launched component to use its own local logging.
|
||||
*/
|
||||
private def forceLoggerConnectionArguments(): Seq[String] = {
|
||||
val connectionSetting = {
|
||||
try {
|
||||
Await.result(loggerConnection, 3.seconds)
|
||||
} catch {
|
||||
case exception: TimeoutException =>
|
||||
Logger[Runtime].warn(
|
||||
"The logger has not been set up within the 3 second time limit, " +
|
||||
"the launched component will be started but it will not be " +
|
||||
"connected to the logging service.",
|
||||
exception
|
||||
)
|
||||
None
|
||||
}
|
||||
}
|
||||
connectionSetting match {
|
||||
case Some(uri) => Seq("--logger-connect", uri.toString)
|
||||
case None => Seq()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ package org.enso.launcher.config
|
||||
import java.io.BufferedWriter
|
||||
import java.nio.file.{Files, NoSuchFileException, Path}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import io.circe.syntax._
|
||||
import io.circe.{yaml, Json}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.components.ComponentsManager
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
|
||||
@ -22,6 +22,8 @@ class GlobalConfigurationManager(
|
||||
distributionManager: DistributionManager
|
||||
) {
|
||||
|
||||
private val logger = Logger[GlobalConfigurationManager]
|
||||
|
||||
/**
|
||||
* Returns the default Enso version that should be used when running Enso
|
||||
* outside a project and when creating a new project.
|
||||
@ -43,7 +45,7 @@ class GlobalConfigurationManager(
|
||||
.lastOption
|
||||
latestInstalled.getOrElse {
|
||||
val latestAvailable = componentsManager.fetchLatestEngineVersion()
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"No Enso versions installed, defaulting to the latest available " +
|
||||
s"release: $latestAvailable."
|
||||
)
|
||||
@ -69,7 +71,7 @@ class GlobalConfigurationManager(
|
||||
.readConfig(configLocation)
|
||||
.recoverWith {
|
||||
case _: NoSuchFileException =>
|
||||
Logger.debug(
|
||||
logger.debug(
|
||||
s"Global config (at ${configLocation.toAbsolutePath} not found, " +
|
||||
s"falling back to defaults."
|
||||
)
|
||||
|
@ -1,20 +1,33 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.{Charset, StandardCharsets}
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.http.client.config.{CookieSpecs, RequestConfig}
|
||||
import org.apache.http.client.methods.HttpUriRequest
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.{Header, HttpResponse}
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.headers.Location
|
||||
import akka.http.scaladsl.model.{HttpRequest, HttpResponse}
|
||||
import akka.stream.scaladsl.{FileIO, Sink}
|
||||
import akka.util.ByteString
|
||||
import com.typesafe.config.{ConfigFactory, ConfigValueFactory}
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.cli.{TaskProgress, TaskProgressImplementation}
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.internal.{ProgressInputStream, ReadProgress}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Using}
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* Represents a HTTP header.
|
||||
*/
|
||||
case class Header(name: String, value: String) {
|
||||
|
||||
/**
|
||||
* Checks if this header instance corresponds to a `headerName`.
|
||||
*
|
||||
* The check is case-insensitive.
|
||||
*/
|
||||
def is(headerName: String): Boolean =
|
||||
name.toLowerCase == headerName.toLowerCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the response contents as a string alongside with the headers
|
||||
@ -29,6 +42,12 @@ case class APIResponse(content: String, headers: Seq[Header])
|
||||
* Contains utility functions for fetching data using the HTTP(S) protocol.
|
||||
*/
|
||||
object HTTPDownload {
|
||||
private val logger = Logger[HTTPDownload.type]
|
||||
|
||||
/**
|
||||
* Determines how many redirects are taken until an error is thrown.
|
||||
*/
|
||||
val maximumRedirects: Int = 20
|
||||
|
||||
/**
|
||||
* Fetches the `request` and tries to decode is as a [[String]].
|
||||
@ -37,6 +56,9 @@ object HTTPDownload {
|
||||
* is returned immediately which can be used to track progress of the
|
||||
* download. The result contains the decoded response and included headers.
|
||||
*
|
||||
* Handles redirects, but will return an error if the amount of redirects
|
||||
* exceeds [[maximumRedirects]].
|
||||
*
|
||||
* @param request the request to send
|
||||
* @param sizeHint an optional hint indicating the expected size of the
|
||||
* response. It is used if the response does not include
|
||||
@ -47,12 +69,23 @@ object HTTPDownload {
|
||||
* be used to get the final result
|
||||
*/
|
||||
def fetchString(
|
||||
request: HttpUriRequest,
|
||||
request: HTTPRequest,
|
||||
sizeHint: Option[Long] = None,
|
||||
encoding: Charset = StandardCharsets.UTF_8
|
||||
): TaskProgress[APIResponse] = {
|
||||
Logger.debug(s"Fetching ${request.getURI.toASCIIString}")
|
||||
runRequest(request, asStringResponseHandler(sizeHint, encoding))
|
||||
logger.debug(s"Fetching ${request.requestImpl.getUri}")
|
||||
def combineChunks(chunks: Seq[ByteString]): String =
|
||||
chunks.reduceOption(_ ++ _).map(_.decodeString(encoding)).getOrElse("")
|
||||
runRequest(
|
||||
request.requestImpl,
|
||||
sizeHint,
|
||||
Sink.seq,
|
||||
(response, chunks: Seq[ByteString]) =>
|
||||
APIResponse(
|
||||
combineChunks(chunks),
|
||||
response.headers.map(header => Header(header.name, header.value))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,6 +97,9 @@ object HTTPDownload {
|
||||
* download. The result is the same path as `destination`. It is available
|
||||
* only when the download has been completed successfully.
|
||||
*
|
||||
* Handles redirects, but will return an error if the amount of redirects
|
||||
* exceeds [[maximumRedirects]].
|
||||
*
|
||||
* @param request the request to send
|
||||
* @param sizeHint an optional hint indicating the expected size of the
|
||||
* response. It is used if the response does not include
|
||||
@ -72,104 +108,120 @@ object HTTPDownload {
|
||||
* be used to wait for the completion of the download.
|
||||
*/
|
||||
def download(
|
||||
request: HttpUriRequest,
|
||||
request: HTTPRequest,
|
||||
destination: Path,
|
||||
sizeHint: Option[Long] = None
|
||||
): TaskProgress[Path] = {
|
||||
Logger.debug(s"Downloading ${request.getURI.toASCIIString} to $destination")
|
||||
runRequest(request, asFileHandler(destination, sizeHint))
|
||||
logger.debug(
|
||||
s"Downloading ${request.requestImpl.getUri} to $destination"
|
||||
)
|
||||
runRequest(
|
||||
request.requestImpl,
|
||||
sizeHint,
|
||||
FileIO.toPath(destination),
|
||||
(_, _: Any) => destination
|
||||
)
|
||||
}
|
||||
|
||||
implicit private lazy val actorSystem: ActorSystem = {
|
||||
val config = ConfigFactory
|
||||
.load()
|
||||
.withValue("akka.loglevel", ConfigValueFactory.fromAnyRef("WARNING"))
|
||||
ActorSystem(
|
||||
"http-requests-actor-system",
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new thread that will send the provided request and returns a
|
||||
* [[TaskProgress]] monitoring progress of that request.
|
||||
* Starts the request and returns a [[TaskProgress]] that can be used to
|
||||
* track download progress and get the result.
|
||||
*
|
||||
* @param request the request to send
|
||||
* @param handler a handler that processes the the received response to
|
||||
* produce some result. The second argument of the handler is
|
||||
* a callback that should be used by the handler to report
|
||||
* progress.
|
||||
* @tparam A type of the result generated on success
|
||||
* @return [[TaskProgress]] that monitors request progress and will return
|
||||
* the response returned by the `handler`
|
||||
* @tparam A type of the result returned by `sink`
|
||||
* @tparam B type of the final result that will be contained in the returned
|
||||
* [[TaskProgress]]
|
||||
* @param request the request to start
|
||||
* @param sizeHint an optional hint indicating the expected size of the
|
||||
* response. It is used if the response does not include
|
||||
* explicit Content-Length header.
|
||||
* @param sink specifies how the response content should be handled, it
|
||||
* receives chunks of [[ByteString]] and should produce a
|
||||
* [[Future]] with some result
|
||||
* @param resultMapping maps the `sink` result and the response into a final
|
||||
* result type, it can use the response instance to for
|
||||
* example, access the headers, but the entity cannot be
|
||||
* used as it will already be drained
|
||||
* @return a [[TaskProgress]] that will contain the final result or any
|
||||
* errors
|
||||
*/
|
||||
private def runRequest[A](
|
||||
request: HttpUriRequest,
|
||||
handler: (HttpResponse, ReadProgress => Unit) => A
|
||||
): TaskProgress[A] = {
|
||||
val task = new TaskProgressImplementation[A]
|
||||
def update(progress: ReadProgress): Unit = {
|
||||
task.reportProgress(progress.alreadyRead(), progress.total())
|
||||
}
|
||||
|
||||
def run(): Unit = {
|
||||
try {
|
||||
val client = buildClient()
|
||||
Using(client.execute(request)) { response =>
|
||||
val result = handler(response, update)
|
||||
task.setComplete(Success(result))
|
||||
}.get
|
||||
} catch {
|
||||
case NonFatal(e) => task.setComplete(Failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
val thread = new Thread(() => run(), "HTTP-Runner")
|
||||
thread.start()
|
||||
task
|
||||
}
|
||||
|
||||
private def buildClient() =
|
||||
HttpClients
|
||||
.custom()
|
||||
.setDefaultRequestConfig(
|
||||
RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()
|
||||
)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Creates a handler that tries to decode the result content as a [[String]].
|
||||
*/
|
||||
private def asStringResponseHandler(
|
||||
private def runRequest[A, B](
|
||||
request: HttpRequest,
|
||||
sizeHint: Option[Long],
|
||||
charset: Charset
|
||||
)(response: HttpResponse, update: ReadProgress => Unit): APIResponse =
|
||||
Using(streamOfResponse(response, update, sizeHint)) { in =>
|
||||
val bytes = in.readAllBytes()
|
||||
val content = new String(bytes, charset)
|
||||
APIResponse(content, response.getAllHeaders.toIndexedSeq)
|
||||
}.get
|
||||
sink: Sink[ByteString, Future[A]],
|
||||
resultMapping: (HttpResponse, A) => B
|
||||
): TaskProgress[B] = {
|
||||
val taskProgress = new TaskProgressImplementation[B]
|
||||
val total = new java.util.concurrent.atomic.AtomicLong(0)
|
||||
import actorSystem.dispatcher
|
||||
|
||||
/**
|
||||
* Creates a handler that tries to save the result content into a file.
|
||||
*/
|
||||
private def asFileHandler(
|
||||
path: Path,
|
||||
sizeHint: Option[Long]
|
||||
)(response: HttpResponse, update: ReadProgress => Unit): Path =
|
||||
Using(streamOfResponse(response, update, sizeHint)) { in =>
|
||||
Using(new FileOutputStream(path.toFile)) { out =>
|
||||
IOUtils.copy(in, out)
|
||||
path
|
||||
}.get
|
||||
}.get
|
||||
val http = Http()
|
||||
|
||||
/**
|
||||
* Returns a progress-monitored stream that can be used to read the response
|
||||
* content.
|
||||
*/
|
||||
private def streamOfResponse(
|
||||
response: HttpResponse,
|
||||
update: ReadProgress => Unit,
|
||||
sizeHint: Option[Long]
|
||||
): ProgressInputStream = {
|
||||
val entity = response.getEntity
|
||||
val size = {
|
||||
val len = entity.getContentLength
|
||||
if (len < 0) None
|
||||
else Some(len)
|
||||
def handleRedirects(retriesLeft: Int)(
|
||||
response: HttpResponse
|
||||
): Future[HttpResponse] =
|
||||
if (response.status.isRedirection) {
|
||||
response.discardEntityBytes()
|
||||
if (retriesLeft > 0) {
|
||||
val newURI = response
|
||||
.header[Location]
|
||||
.getOrElse(
|
||||
throw HTTPException(
|
||||
s"HTTP response was ${response.status} which indicates a " +
|
||||
s"redirection, but the Location header was missing or invalid."
|
||||
)
|
||||
)
|
||||
.uri
|
||||
if (newURI.scheme != "https") {
|
||||
throw HTTPException(
|
||||
s"The HTTP redirection would result in a non-HTTPS connection " +
|
||||
s"(the requested scheme was `${newURI.scheme}`). " +
|
||||
"This is not safe, the request has been terminated."
|
||||
)
|
||||
}
|
||||
|
||||
logger.trace(
|
||||
s"HTTP response was ${response.status}, redirecting to `$newURI`."
|
||||
)
|
||||
val newRequest = request.withUri(newURI)
|
||||
http
|
||||
.singleRequest(newRequest)
|
||||
.flatMap(handleRedirects(retriesLeft - 1))
|
||||
} else {
|
||||
throw HTTPException("Too many redirects.")
|
||||
}
|
||||
} else Future.successful(response)
|
||||
|
||||
def handleFinalResponse(
|
||||
response: HttpResponse
|
||||
): Future[(HttpResponse, A)] = {
|
||||
val sizeEstimate =
|
||||
response.entity.contentLengthOption.orElse(sizeHint)
|
||||
response.entity.dataBytes
|
||||
.map { chunk =>
|
||||
val currentTotal = total.addAndGet(chunk.size.toLong)
|
||||
taskProgress.reportProgress(currentTotal, sizeEstimate)
|
||||
chunk
|
||||
}
|
||||
.runWith(sink)
|
||||
.map((response, _))
|
||||
}
|
||||
|
||||
new ProgressInputStream(entity.getContent, size.orElse(sizeHint), update)
|
||||
http
|
||||
.singleRequest(request)
|
||||
.flatMap(handleRedirects(maximumRedirects))
|
||||
.flatMap(handleFinalResponse)
|
||||
.map(resultMapping.tupled)
|
||||
.onComplete(taskProgress.setComplete)
|
||||
taskProgress
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
/**
|
||||
* Indicates an error when processing a HTTP request.
|
||||
*/
|
||||
case class HTTPException(message: String) extends RuntimeException(message)
|
@ -0,0 +1,9 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
import akka.http.scaladsl.model.HttpRequest
|
||||
|
||||
/**
|
||||
* Wraps an underlying HTTP request implementation to make the outside API
|
||||
* independent of the internal implementation.
|
||||
*/
|
||||
case class HTTPRequest(requestImpl: HttpRequest)
|
@ -1,8 +1,7 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
import java.net.URI
|
||||
|
||||
import org.apache.http.client.methods.{HttpUriRequest, RequestBuilder}
|
||||
import akka.http.scaladsl.model.HttpHeader.ParsingResult
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
/**
|
||||
* A simple immutable builder for HTTP requests.
|
||||
@ -11,14 +10,14 @@ import org.apache.http.client.methods.{HttpUriRequest, RequestBuilder}
|
||||
* the launcher. It can be easily extended if necessary.
|
||||
*/
|
||||
case class HTTPRequestBuilder private (
|
||||
uri: URI,
|
||||
uri: Uri,
|
||||
headers: Vector[(String, String)]
|
||||
) {
|
||||
|
||||
/**
|
||||
* Builds a GET request with the specified settings.
|
||||
*/
|
||||
def GET: HttpUriRequest = build(RequestBuilder.get())
|
||||
def GET: HTTPRequest = build(HttpMethods.GET)
|
||||
|
||||
/**
|
||||
* Adds an additional header that will be included in the request.
|
||||
@ -29,12 +28,22 @@ case class HTTPRequestBuilder private (
|
||||
def addHeader(name: String, value: String): HTTPRequestBuilder =
|
||||
copy(headers = headers.appended((name, value)))
|
||||
|
||||
private def build(requestBuilder: RequestBuilder): HttpUriRequest = {
|
||||
val withUri = requestBuilder.setUri(uri)
|
||||
val withHeaders = headers.foldLeft(withUri)((builder, header) =>
|
||||
builder.addHeader(header._1, header._2)
|
||||
)
|
||||
withHeaders.build()
|
||||
private def build(
|
||||
method: HttpMethod
|
||||
): HTTPRequest = {
|
||||
val httpHeaders = headers.map {
|
||||
case (name, value) =>
|
||||
HttpHeader.parse(name, value) match {
|
||||
case ParsingResult.Ok(header, errors) if errors.isEmpty =>
|
||||
header
|
||||
case havingErrors =>
|
||||
throw new IllegalStateException(
|
||||
s"Internal error: " +
|
||||
s"Invalid value for header $name: ${havingErrors.errors}."
|
||||
)
|
||||
}
|
||||
}
|
||||
HTTPRequest(HttpRequest(method = method, uri = uri, headers = httpHeaders))
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,7 +52,7 @@ object HTTPRequestBuilder {
|
||||
/**
|
||||
* Creates a request builder that will send the request for the given URI.
|
||||
*/
|
||||
def fromURI(uri: URI): HTTPRequestBuilder =
|
||||
def fromURI(uri: Uri): HTTPRequestBuilder =
|
||||
new HTTPRequestBuilder(uri, Vector.empty)
|
||||
|
||||
/**
|
||||
@ -51,5 +60,5 @@ object HTTPRequestBuilder {
|
||||
* builder that will send the request to the given `uri`.
|
||||
*/
|
||||
def fromURIString(uri: String): HTTPRequestBuilder =
|
||||
fromURI(new URI(uri))
|
||||
fromURI(Uri.parseAbsolute(uri))
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
import java.net.URI
|
||||
|
||||
import org.apache.http.client.utils.{URIBuilder => ApacheURIBuilder}
|
||||
import akka.http.scaladsl.model.Uri
|
||||
|
||||
/**
|
||||
* A simple immutable builder for URIs based on URLs.
|
||||
@ -13,11 +11,7 @@ import org.apache.http.client.utils.{URIBuilder => ApacheURIBuilder}
|
||||
* As all APIs we use support HTTPS, it does not allow to create a non-HTTPS
|
||||
* URL.
|
||||
*/
|
||||
case class URIBuilder private (
|
||||
host: String,
|
||||
segments: Vector[String],
|
||||
queries: Vector[(String, String)]
|
||||
) {
|
||||
case class URIBuilder private (uri: Uri) {
|
||||
|
||||
/**
|
||||
* Resolve a segment over the path in the URI.
|
||||
@ -25,8 +19,10 @@ case class URIBuilder private (
|
||||
* For example adding `bar` to `http://example.com/foo` will result in
|
||||
* `http://example.com/foo/bar`.
|
||||
*/
|
||||
def addPathSegment(segment: String): URIBuilder =
|
||||
copy(segments = segments.appended(segment))
|
||||
def addPathSegment(segment: String): URIBuilder = {
|
||||
val part = "/" + segment
|
||||
copy(uri.withPath(uri.path + part))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a query parameter to the URI.
|
||||
@ -34,21 +30,12 @@ case class URIBuilder private (
|
||||
* The query is appended at the end.
|
||||
*/
|
||||
def addQuery(key: String, value: String): URIBuilder =
|
||||
copy(queries = queries.appended((key, value)))
|
||||
copy(uri.withQuery(uri.query().+:((key, value))))
|
||||
|
||||
/**
|
||||
* Build the URI represented by this builder.
|
||||
*/
|
||||
def build(): URI = {
|
||||
val base = (new ApacheURIBuilder)
|
||||
.setScheme("https")
|
||||
.setHost(host)
|
||||
.setPathSegments(segments: _*)
|
||||
val withQueries = queries.foldLeft(base)((builder, query) =>
|
||||
builder.addParameter(query._1, query._2)
|
||||
)
|
||||
withQueries.build()
|
||||
}
|
||||
def build(): Uri = uri
|
||||
}
|
||||
|
||||
object URIBuilder {
|
||||
@ -60,19 +47,22 @@ object URIBuilder {
|
||||
* `https://example.com/`.
|
||||
*/
|
||||
def fromHost(host: String): URIBuilder =
|
||||
new URIBuilder(
|
||||
host = host,
|
||||
segments = Vector.empty,
|
||||
queries = Vector.empty
|
||||
)
|
||||
new URIBuilder(Uri.from(scheme = "https", host = host))
|
||||
|
||||
/**
|
||||
* A simple DSL for the URIBuilder.
|
||||
*/
|
||||
implicit class URIBuilderSyntax(builder: URIBuilder) {
|
||||
|
||||
/**
|
||||
* Extends the URI with an additional path segment.
|
||||
*/
|
||||
def /(part: String): URIBuilder =
|
||||
builder.addPathSegment(part)
|
||||
|
||||
/**
|
||||
* Adds a query to the URI.
|
||||
*/
|
||||
def ?(query: (String, String)): URIBuilder =
|
||||
builder.addQuery(query._1, query._2)
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ package org.enso.launcher.installation
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.cli.{GlobalCLIOptions, InternalOpts}
|
||||
import org.enso.launcher.cli.{GlobalCLIOptions, InternalOpts, Main}
|
||||
import org.enso.launcher.config.GlobalConfigurationManager
|
||||
import org.enso.launcher.installation.DistributionInstaller.{
|
||||
BundleAction,
|
||||
@ -12,7 +13,7 @@ import org.enso.launcher.installation.DistributionInstaller.{
|
||||
MoveBundles
|
||||
}
|
||||
import org.enso.launcher.locking.{DefaultResourceManager, ResourceManager}
|
||||
import org.enso.launcher.{FileSystem, Logger, OS}
|
||||
import org.enso.launcher.{FileSystem, InfoLogger, OS}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
@ -38,6 +39,7 @@ class DistributionInstaller(
|
||||
removeOldLauncher: Boolean,
|
||||
bundleActionOption: Option[DistributionInstaller.BundleAction]
|
||||
) {
|
||||
private val logger = Logger[DistributionInstaller]
|
||||
final private val installed = manager.LocallyInstalledDirectories
|
||||
private val env = manager.env
|
||||
|
||||
@ -65,7 +67,7 @@ class DistributionInstaller(
|
||||
try {
|
||||
val settings = prepare()
|
||||
resourceManager.acquireExclusiveMainLock(waitAction = () => {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
"No other Enso processes associated with this distribution can be " +
|
||||
"running during the installation. The installer will wait until " +
|
||||
"other Enso processes are terminated."
|
||||
@ -74,7 +76,7 @@ class DistributionInstaller(
|
||||
installBinary()
|
||||
createDirectoryStructure()
|
||||
installBundles(settings.bundleAction)
|
||||
Logger.info("Installation succeeded.")
|
||||
InfoLogger.info("Installation succeeded.")
|
||||
|
||||
if (settings.removeInstaller) {
|
||||
removeInstaller()
|
||||
@ -82,9 +84,9 @@ class DistributionInstaller(
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
val message = s"Installation failed with error: $e."
|
||||
Logger.error(message, e)
|
||||
logger.error(message, e)
|
||||
CLIOutput.println(message)
|
||||
sys.exit(1)
|
||||
Main.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,49 +107,46 @@ class DistributionInstaller(
|
||||
*/
|
||||
private def prepare(): InstallationSettings = {
|
||||
if (installedLauncherPath == currentLauncherPath) {
|
||||
Logger.error(
|
||||
throw InstallationError(
|
||||
"The installation source and destination are the same. Nothing to " +
|
||||
"install."
|
||||
)
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
if (Files.exists(installed.dataDirectory)) {
|
||||
Logger.error(
|
||||
throw InstallationError(
|
||||
s"${installed.dataDirectory} already exists. " +
|
||||
s"Please uninstall the already existing distribution. " +
|
||||
s"If no distribution is installed, please remove the directory " +
|
||||
s"${installed.dataDirectory} before installing."
|
||||
)
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
if (
|
||||
Files.exists(installed.configDirectory) &&
|
||||
installed.configDirectory.toFile.exists()
|
||||
) {
|
||||
Logger.warn(s"${installed.configDirectory} already exists.")
|
||||
logger.warn(s"${installed.configDirectory} already exists.")
|
||||
if (!Files.isDirectory(installed.configDirectory)) {
|
||||
Logger.error(
|
||||
throw InstallationError(
|
||||
s"${installed.configDirectory} already exists but is not a " +
|
||||
s"directory. Please remove it or change the installation " +
|
||||
s"location by setting `${installed.ENSO_CONFIG_DIRECTORY}`."
|
||||
)
|
||||
sys.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
for (file <- nonEssentialFiles) {
|
||||
val f = manager.paths.dataRoot / file
|
||||
if (!Files.exists(f)) {
|
||||
Logger.warn(s"$f does not exist, it will be skipped.")
|
||||
logger.warn(s"$f does not exist, it will be skipped.")
|
||||
}
|
||||
}
|
||||
|
||||
for (dir <- nonEssentialDirectories) {
|
||||
val f = manager.paths.dataRoot / dir
|
||||
if (!Files.isDirectory(f)) {
|
||||
Logger.warn(s"$f does not exist, it will be skipped.")
|
||||
logger.warn(s"$f does not exist, it will be skipped.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,8 +174,8 @@ class DistributionInstaller(
|
||||
|
||||
val additionalMessages = pathMessage.toSeq ++ binMessage.toSeq
|
||||
|
||||
Logger.info(message)
|
||||
additionalMessages.foreach(msg => Logger.warn(msg))
|
||||
InfoLogger.info(message)
|
||||
additionalMessages.foreach(msg => logger.warn(msg))
|
||||
|
||||
val removeInstaller = decideIfInstallerShouldBeRemoved()
|
||||
val bundleAction = decideBundleAction()
|
||||
@ -189,7 +188,7 @@ class DistributionInstaller(
|
||||
)
|
||||
if (!proceed) {
|
||||
CLIOutput.println("Installation has been cancelled.")
|
||||
sys.exit(1)
|
||||
Main.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,6 +215,7 @@ class DistributionInstaller(
|
||||
installed.binaryExecutable
|
||||
)
|
||||
FileSystem.ensureIsExecutable(installed.binaryExecutable)
|
||||
logger.debug("Binary installed.")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -250,13 +250,14 @@ class DistributionInstaller(
|
||||
private def copyNonEssentialFiles(): Unit = {
|
||||
for (file <- nonEssentialFiles) {
|
||||
try {
|
||||
logger.trace(s"Copying `$file`.")
|
||||
FileSystem.copyFile(
|
||||
manager.paths.dataRoot / file,
|
||||
installed.dataDirectory / file
|
||||
)
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"An exception $e prevented some non-essential " +
|
||||
s"documentation files from being copied."
|
||||
)
|
||||
@ -271,7 +272,7 @@ class DistributionInstaller(
|
||||
)
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"An exception $e prevented some non-essential directories from " +
|
||||
s"being copied."
|
||||
)
|
||||
@ -324,7 +325,7 @@ class DistributionInstaller(
|
||||
} else {
|
||||
bundleActionOption match {
|
||||
case Some(value) if value != DistributionInstaller.IgnoreBundles =>
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"Installer was asked to ${value.description}, but it seems to " +
|
||||
s"not be running from a bundle package."
|
||||
)
|
||||
@ -345,14 +346,16 @@ class DistributionInstaller(
|
||||
if (runtimes.length + engines.length > 0) {
|
||||
if (bundleAction.copy) {
|
||||
for (engine <- engines) {
|
||||
Logger.info(s"Copying bundled Enso engine ${engine.getFileName}.")
|
||||
InfoLogger.info(
|
||||
s"Copying bundled Enso engine ${engine.getFileName}."
|
||||
)
|
||||
FileSystem.copyDirectory(
|
||||
engine,
|
||||
enginesDirectory / engine.getFileName
|
||||
)
|
||||
}
|
||||
for (runtime <- runtimes) {
|
||||
Logger.info(s"Copying bundled runtime ${runtime.getFileName}.")
|
||||
InfoLogger.info(s"Copying bundled runtime ${runtime.getFileName}.")
|
||||
FileSystem.copyDirectory(
|
||||
runtime,
|
||||
runtimesDirectory / runtime.getFileName
|
||||
@ -364,14 +367,14 @@ class DistributionInstaller(
|
||||
engines.foreach(FileSystem.removeDirectory)
|
||||
runtimes.foreach(FileSystem.removeDirectory)
|
||||
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
"Cleaned bundled files from the original distribution packages."
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (bundleAction != IgnoreBundles) {
|
||||
throw new IllegalStateException(
|
||||
s"Internal error: The runner is not run in portable mode, " +
|
||||
s"Internal error: The launcher is not run in portable mode, " +
|
||||
s"but the final bundle action was not Ignore, but $bundleAction."
|
||||
)
|
||||
}
|
||||
@ -406,7 +409,7 @@ class DistributionInstaller(
|
||||
} else {
|
||||
Files.delete(currentLauncherPath)
|
||||
}
|
||||
sys.exit()
|
||||
Main.exit(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ package org.enso.launcher.installation
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.locking.{DefaultResourceManager, ResourceManager}
|
||||
import org.enso.launcher.{Environment, FileSystem, Logger, OS}
|
||||
import org.enso.launcher.{Environment, FileSystem, InfoLogger, OS}
|
||||
|
||||
import scala.util.Try
|
||||
import scala.util.control.NonFatal
|
||||
@ -21,6 +22,7 @@ import scala.util.control.NonFatal
|
||||
* @param config location of configuration
|
||||
* @param locks a directory for storing lockfiles that are used to synchronize
|
||||
* access to the various components
|
||||
* @param logs a directory for storing logs
|
||||
* @param tmp a directory for storing temporary files that is located on the
|
||||
* same filesystem as `runtimes` and `engines`, used during
|
||||
* installation to decrease the possibility of getting a broken
|
||||
@ -38,6 +40,7 @@ case class DistributionPaths(
|
||||
engines: Path,
|
||||
config: Path,
|
||||
locks: Path,
|
||||
logs: Path,
|
||||
tmp: Path,
|
||||
resourceManager: ResourceManager
|
||||
) {
|
||||
@ -69,6 +72,7 @@ class DistributionManager(
|
||||
val env: Environment,
|
||||
resourceManager: ResourceManager
|
||||
) {
|
||||
private val logger = Logger[DistributionManager]
|
||||
|
||||
/**
|
||||
* Specifies whether the launcher has been run as a portable distribution or
|
||||
@ -76,24 +80,24 @@ class DistributionManager(
|
||||
*/
|
||||
lazy val isRunningPortable: Boolean = {
|
||||
val portable = detectPortable()
|
||||
Logger.debug(s"Launcher portable mode = $portable")
|
||||
logger.debug(s"Launcher portable mode = $portable")
|
||||
if (portable && LocallyInstalledDirectories.installedDistributionExists) {
|
||||
val installedRoot = LocallyInstalledDirectories.dataDirectory
|
||||
val installedBinary = LocallyInstalledDirectories.binaryExecutable
|
||||
|
||||
Logger.debug(
|
||||
logger.debug(
|
||||
s"The launcher is run in portable mode, but an installed distribution" +
|
||||
s" is available at $installedRoot."
|
||||
)
|
||||
|
||||
if (Files.exists(installedBinary)) {
|
||||
if (installedBinary == env.getPathToRunningExecutable) {
|
||||
Logger.debug(
|
||||
logger.debug(
|
||||
"That distribution seems to be corresponding to this launcher " +
|
||||
"executable, that is running in portable mode."
|
||||
)
|
||||
} else {
|
||||
Logger.debug(
|
||||
logger.debug(
|
||||
s"However, that installed distribution most likely uses another " +
|
||||
s"launcher executable, located at $installedBinary."
|
||||
)
|
||||
@ -108,7 +112,7 @@ class DistributionManager(
|
||||
*/
|
||||
lazy val paths: DistributionPaths = {
|
||||
val paths = detectPaths()
|
||||
Logger.debug(s"Detected paths are: $paths")
|
||||
logger.debug(s"Detected paths are: $paths")
|
||||
paths
|
||||
}
|
||||
|
||||
@ -118,6 +122,7 @@ class DistributionManager(
|
||||
val CONFIG_DIRECTORY = "config"
|
||||
val BIN_DIRECTORY = "bin"
|
||||
val LOCK_DIRECTORY = "lock"
|
||||
val LOG_DIRECTORY = "log"
|
||||
val TMP_DIRECTORY = "tmp"
|
||||
|
||||
private def detectPortable(): Boolean = Files.exists(portableMarkFilePath)
|
||||
@ -136,6 +141,7 @@ class DistributionManager(
|
||||
engines = root / ENGINES_DIRECTORY,
|
||||
config = root / CONFIG_DIRECTORY,
|
||||
locks = root / LOCK_DIRECTORY,
|
||||
logs = root / LOG_DIRECTORY,
|
||||
tmp = root / TMP_DIRECTORY,
|
||||
resourceManager
|
||||
)
|
||||
@ -149,6 +155,7 @@ class DistributionManager(
|
||||
engines = dataRoot / ENGINES_DIRECTORY,
|
||||
config = configRoot,
|
||||
locks = runRoot / LOCK_DIRECTORY,
|
||||
logs = LocallyInstalledDirectories.logDirectory,
|
||||
tmp = dataRoot / TMP_DIRECTORY,
|
||||
resourceManager
|
||||
)
|
||||
@ -168,7 +175,7 @@ class DistributionManager(
|
||||
if (Files.exists(tmp)) {
|
||||
resourceManager.tryWithExclusiveTemporaryDirectory {
|
||||
if (!FileSystem.isDirectoryEmpty(tmp)) {
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
"Cleaning up temporary files from a previous installation."
|
||||
)
|
||||
}
|
||||
@ -187,7 +194,7 @@ class DistributionManager(
|
||||
for (lockfile <- lockfiles) {
|
||||
try {
|
||||
Files.delete(lockfile)
|
||||
Logger.debug(s"Removed unused lockfile ${lockfile.getFileName}.")
|
||||
logger.debug(s"Removed unused lockfile ${lockfile.getFileName}.")
|
||||
} catch {
|
||||
case NonFatal(_) =>
|
||||
}
|
||||
@ -206,11 +213,13 @@ class DistributionManager(
|
||||
val ENSO_CONFIG_DIRECTORY = "ENSO_CONFIG_DIRECTORY"
|
||||
val ENSO_BIN_DIRECTORY = "ENSO_BIN_DIRECTORY"
|
||||
val ENSO_RUNTIME_DIRECTORY = "ENSO_RUNTIME_DIRECTORY"
|
||||
val ENSO_LOG_DIRECTORY = "ENSO_LOG_DIRECTORY"
|
||||
|
||||
private val XDG_DATA_DIRECTORY = "XDG_DATA_HOME"
|
||||
private val XDG_CONFIG_DIRECTORY = "XDG_CONFIG_HOME"
|
||||
private val XDG_BIN_DIRECTORY = "XDG_BIN_HOME"
|
||||
private val XDG_RUN_DIRECTORY = "XDG_RUNTIME_DIR"
|
||||
private val XDG_CACHE_DIRECTORY = "XDG_CACHE_HOME"
|
||||
|
||||
private val LINUX_ENSO_DIRECTORY = "enso"
|
||||
private val MACOS_ENSO_DIRECTORY = "org.enso"
|
||||
@ -239,6 +248,19 @@ class DistributionManager(
|
||||
}
|
||||
.toAbsolutePath
|
||||
|
||||
/**
|
||||
* Returns names of directories that may be located inside of the data
|
||||
* directory.
|
||||
*/
|
||||
def possibleDirectoriesInsideDataDirectory: Seq[String] =
|
||||
Seq(
|
||||
CONFIG_DIRECTORY,
|
||||
TMP_DIRECTORY,
|
||||
LOG_DIRECTORY,
|
||||
LOCK_DIRECTORY,
|
||||
"components-licences"
|
||||
)
|
||||
|
||||
/**
|
||||
* Config directory for an installed distribution.
|
||||
*/
|
||||
@ -302,6 +324,26 @@ class DistributionManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The directory for storing logs.
|
||||
*/
|
||||
def logDirectory: Path =
|
||||
env
|
||||
.getEnvPath(ENSO_LOG_DIRECTORY)
|
||||
.getOrElse {
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux =>
|
||||
env
|
||||
.getEnvPath(XDG_CACHE_DIRECTORY)
|
||||
.map(_ / LINUX_ENSO_DIRECTORY)
|
||||
.getOrElse(dataDirectory / LOG_DIRECTORY)
|
||||
case OS.MacOS =>
|
||||
env.getHome / "Library" / "Logs" / MACOS_ENSO_DIRECTORY
|
||||
case OS.Windows =>
|
||||
dataDirectory / LOG_DIRECTORY
|
||||
}
|
||||
}
|
||||
|
||||
private def executableName: String =
|
||||
OS.executableName("enso")
|
||||
|
||||
|
@ -2,13 +2,19 @@ package org.enso.launcher.installation
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.cli.{GlobalCLIOptions, InternalOpts}
|
||||
import org.enso.launcher.cli.{
|
||||
GlobalCLIOptions,
|
||||
InternalOpts,
|
||||
LauncherLogging,
|
||||
Main
|
||||
}
|
||||
import org.enso.launcher.config.GlobalConfigurationManager
|
||||
import org.enso.launcher.locking.{DefaultResourceManager, ResourceManager}
|
||||
import org.enso.launcher.{FileSystem, Logger, OS}
|
||||
import org.enso.launcher.{FileSystem, InfoLogger, OS}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
@ -17,14 +23,14 @@ import scala.util.control.NonFatal
|
||||
*
|
||||
* @param manager a distribution manager instance which defines locations for
|
||||
* the distribution that will be uninstalled
|
||||
* @param autoConfirm if set to true, the uninstaller will use defaults
|
||||
* instead of asking questions
|
||||
*/
|
||||
class DistributionUninstaller(
|
||||
manager: DistributionManager,
|
||||
resourceManager: ResourceManager,
|
||||
autoConfirm: Boolean
|
||||
globalCLIOptions: GlobalCLIOptions
|
||||
) {
|
||||
private val autoConfirm = globalCLIOptions.autoConfirm
|
||||
private val logger = Logger[DistributionUninstaller]
|
||||
|
||||
/**
|
||||
* Uninstalls a locally installed (non-portable) distribution.
|
||||
@ -40,7 +46,7 @@ class DistributionUninstaller(
|
||||
checkPortable()
|
||||
askConfirmation()
|
||||
resourceManager.acquireExclusiveMainLock(waitAction = () => {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
"Please ensure that no other Enso processes are using this " +
|
||||
"distribution before uninstalling. The uninstaller will resume once " +
|
||||
"all related Enso processes exit."
|
||||
@ -61,7 +67,7 @@ class DistributionUninstaller(
|
||||
uninstallConfig()
|
||||
uninstallExecutableUNIX()
|
||||
uninstallDataContents(deferDataRootRemoval = false)
|
||||
Logger.info("Successfully uninstalled the distribution.")
|
||||
InfoLogger.info("Successfully uninstalled the distribution.")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,7 +84,7 @@ class DistributionUninstaller(
|
||||
uninstallConfig()
|
||||
val newPath = partiallyUninstallExecutableWindows()
|
||||
uninstallDataContents(deferRootRemoval)
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
"Successfully uninstalled the distribution but for the launcher " +
|
||||
"executable. It will be removed in a moment after this program " +
|
||||
"terminates."
|
||||
@ -99,15 +105,15 @@ class DistributionUninstaller(
|
||||
*/
|
||||
private def checkPortable(): Unit = {
|
||||
if (manager.isRunningPortable) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
"The Enso distribution you are currently running is in portable " +
|
||||
"mode, so it cannot be uninstalled."
|
||||
)
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"If you still want to remove it, you can just remove the " +
|
||||
s"`${manager.paths.dataRoot}` directory."
|
||||
)
|
||||
sys.exit(1)
|
||||
Main.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,12 +122,12 @@ class DistributionUninstaller(
|
||||
* will be removed and asks the user if they want to proceed.
|
||||
*/
|
||||
private def askConfirmation(): Unit = {
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"Uninstalling this distribution will remove the launcher located at " +
|
||||
s"`${manager.env.getPathToRunningExecutable}`, all engine and runtime " +
|
||||
s"components and configuration managed by this distribution."
|
||||
)
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"ENSO_DATA_DIRECTORY (${manager.paths.dataRoot}) and " +
|
||||
s"ENSO_CONFIG_DIRECTORY (${manager.paths.config}) will be removed " +
|
||||
s"unless they contain unexpected files."
|
||||
@ -130,8 +136,8 @@ class DistributionUninstaller(
|
||||
val proceed =
|
||||
CLIOutput.askConfirmation("Do you want to proceed?", yesDefault = true)
|
||||
if (!proceed) {
|
||||
Logger.warn("Installation has been cancelled on user request.")
|
||||
sys.exit(1)
|
||||
logger.warn("Uninstallation has been cancelled on user request.")
|
||||
Main.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -173,13 +179,13 @@ class DistributionUninstaller(
|
||||
private val knownDataFiles = Seq("README.md", "NOTICE")
|
||||
|
||||
/**
|
||||
* Directories that are expected to be inside of the data root.
|
||||
* Directories that are expected to be inside of the data root, except for
|
||||
* the locks directory which is handled separately.
|
||||
*/
|
||||
private val knownDataDirectories = Seq(
|
||||
manager.TMP_DIRECTORY,
|
||||
manager.CONFIG_DIRECTORY,
|
||||
"components-licences"
|
||||
)
|
||||
private val knownDataDirectories =
|
||||
Set.from(
|
||||
manager.LocallyInstalledDirectories.possibleDirectoriesInsideDataDirectory
|
||||
) - manager.LOCK_DIRECTORY
|
||||
|
||||
/**
|
||||
* Removes all files contained in the ENSO_DATA_DIRECTORY and possibly the
|
||||
@ -195,6 +201,14 @@ class DistributionUninstaller(
|
||||
FileSystem.removeDirectory(manager.paths.runtimes)
|
||||
|
||||
val dataRoot = manager.paths.dataRoot
|
||||
|
||||
val logsInsideData = manager.paths.logs.toAbsolutePath.normalize.startsWith(
|
||||
dataRoot.toAbsolutePath.normalize
|
||||
)
|
||||
if (logsInsideData) {
|
||||
LauncherLogging.prepareForUninstall(globalCLIOptions)
|
||||
}
|
||||
|
||||
for (dirName <- knownDataDirectories) {
|
||||
FileSystem.removeDirectoryIfExists(dataRoot / dirName)
|
||||
}
|
||||
@ -212,7 +226,7 @@ class DistributionUninstaller(
|
||||
Files.delete(lock)
|
||||
} catch {
|
||||
case NonFatal(exception) =>
|
||||
Logger.error(
|
||||
logger.error(
|
||||
s"Cannot remove lockfile ${lock.getFileName}.",
|
||||
exception
|
||||
)
|
||||
@ -267,12 +281,12 @@ class DistributionUninstaller(
|
||||
def remainingFilesList =
|
||||
remainingFiles.map(fileName => s"`$fileName`").mkString(", ")
|
||||
if (autoConfirm) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"$directoryName ($path) contains unexpected files: " +
|
||||
s"$remainingFilesList, so it will not be removed."
|
||||
)
|
||||
} else {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"$directoryName ($path) contains unexpected files: " +
|
||||
s"$remainingFilesList."
|
||||
)
|
||||
@ -349,6 +363,6 @@ object DistributionUninstaller {
|
||||
new DistributionUninstaller(
|
||||
DistributionManager,
|
||||
DefaultResourceManager,
|
||||
globalCLIOptions.autoConfirm
|
||||
globalCLIOptions
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package org.enso.launcher.installation
|
||||
|
||||
/**
|
||||
* Indicates an installation failure.
|
||||
*
|
||||
* Possibly installation being manually cancelled.
|
||||
*/
|
||||
case class InstallationError(message: String)
|
||||
extends RuntimeException(message) {
|
||||
override def toString: String = message
|
||||
}
|
@ -3,7 +3,7 @@ package org.enso.launcher.locking
|
||||
import java.nio.channels.{FileChannel, FileLock}
|
||||
import java.nio.file.{Files, Path, StandardOpenOption}
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
import com.typesafe.scalalogging.Logger
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
@ -107,7 +107,7 @@ abstract class FileLockManager extends LockManager {
|
||||
) extends Lock {
|
||||
|
||||
if (isShared(lockType) && !fileLock.isShared) {
|
||||
Logger.warn(
|
||||
Logger[FileLockManager].warn(
|
||||
"A request for a shared lock returned an exclusive lock. " +
|
||||
"The platform that you are running on may not support shared locks, " +
|
||||
"this may result in only a single Enso instance being able to run at " +
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.launcher.locking
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
import com.typesafe.scalalogging.Logger
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
@ -9,6 +9,8 @@ import scala.util.Using
|
||||
*/
|
||||
class ResourceManager(lockManager: LockManager) {
|
||||
|
||||
private val logger = Logger[ResourceManager]
|
||||
|
||||
/**
|
||||
* Runs the `action` while holding a lock (of `lockType`) for the `resource`.
|
||||
*
|
||||
@ -29,7 +31,7 @@ class ResourceManager(lockManager: LockManager) {
|
||||
() =>
|
||||
waitingAction
|
||||
.map(_.apply(resource))
|
||||
.getOrElse(Logger.warn(resource.waitMessage))
|
||||
.getOrElse(logger.warn(resource.waitMessage))
|
||||
)
|
||||
} { _ => action }.get
|
||||
|
||||
@ -143,7 +145,7 @@ class ResourceManager(lockManager: LockManager) {
|
||||
val lock = lockManager.acquireLockWithWaitingAction(
|
||||
TemporaryDirectory.name,
|
||||
LockType.Shared,
|
||||
() => Logger.warn(TemporaryDirectory.waitMessage)
|
||||
() => logger.warn(TemporaryDirectory.waitMessage)
|
||||
)
|
||||
temporaryDirectoryLock = Some(lock)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package org.enso.launcher.releases
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.launcher.http.URIBuilder
|
||||
import org.enso.launcher.releases.engine.{EngineRelease, EngineReleaseProvider}
|
||||
import org.enso.launcher.releases.fallback.SimpleReleaseProviderWithFallback
|
||||
@ -82,7 +82,7 @@ object EnsoRepository {
|
||||
"release build."
|
||||
)
|
||||
else {
|
||||
Logger.debug(s"[TEST] Using a fake repository at $fakeRepositoryRoot.")
|
||||
Logger("TEST").debug(s"Using a fake repository at $fakeRepositoryRoot.")
|
||||
launcherRepository =
|
||||
makeFakeRepository(fakeRepositoryRoot, shouldWaitForAssets)
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import java.nio.file.Path
|
||||
|
||||
import io.circe._
|
||||
import io.circe.parser._
|
||||
import org.apache.http.Header
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.http.{
|
||||
APIResponse,
|
||||
HTTPDownload,
|
||||
HTTPRequestBuilder,
|
||||
Header,
|
||||
URIBuilder
|
||||
}
|
||||
import org.enso.launcher.releases.ReleaseProviderException
|
||||
@ -134,9 +134,9 @@ object GithubAPI {
|
||||
defaultError: => Throwable
|
||||
): Throwable = {
|
||||
def isLimitExceeded(header: Header): Boolean =
|
||||
header.getValue.toIntOption.contains(0)
|
||||
header.value.toIntOption.contains(0)
|
||||
|
||||
response.headers.find(_.getName == "X-RateLimit-Remaining") match {
|
||||
response.headers.find(_.is("X-RateLimit-Remaining")) match {
|
||||
case Some(header) if isLimitExceeded(header) =>
|
||||
ReleaseProviderException(
|
||||
"GitHub Release API rate limit exceeded for your IP address. " +
|
||||
|
@ -125,14 +125,19 @@ case class FakeAsset(
|
||||
*/
|
||||
private def maybeWaitForAsset(): Unit =
|
||||
if (shouldWaitForAssets) {
|
||||
val name = "testasset-" + fileName
|
||||
val lock = DefaultFileLockManager.acquireLockWithWaitingAction(
|
||||
name,
|
||||
LockType.Shared,
|
||||
waitingAction = () => {
|
||||
System.err.println("INTERNAL-TEST-ACQUIRING-LOCK")
|
||||
val name = "testasset-" + fileName
|
||||
val lockType = LockType.Shared
|
||||
val lock =
|
||||
DefaultFileLockManager.tryAcquireLock(name, lockType) match {
|
||||
case Some(immediateLock) =>
|
||||
System.err.println(
|
||||
"[TEST] Error: Lock was unexpectedly acquired immediately."
|
||||
)
|
||||
immediateLock
|
||||
case None =>
|
||||
System.err.println("INTERNAL-TEST-ACQUIRING-LOCK")
|
||||
DefaultFileLockManager.acquireLock(name, lockType)
|
||||
}
|
||||
)
|
||||
lock.release()
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package org.enso.launcher.upgrade
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
@ -17,7 +18,8 @@ import org.enso.launcher.locking.{
|
||||
}
|
||||
import org.enso.launcher.releases.launcher.LauncherRelease
|
||||
import org.enso.launcher.releases.{EnsoRepository, ReleaseProvider}
|
||||
import org.enso.launcher.{CurrentVersion, FileSystem, Logger, OS}
|
||||
import org.enso.launcher.{CurrentVersion, FileSystem, InfoLogger, OS}
|
||||
import org.enso.logger.LoggerSyntax
|
||||
|
||||
import scala.util.Try
|
||||
import scala.util.control.NonFatal
|
||||
@ -30,6 +32,8 @@ class LauncherUpgrader(
|
||||
originalExecutablePath: Option[Path]
|
||||
) {
|
||||
|
||||
private val logger = Logger[LauncherUpgrader]
|
||||
|
||||
/**
|
||||
* Queries the release provider for the latest available valid launcher
|
||||
* version.
|
||||
@ -57,14 +61,14 @@ class LauncherUpgrader(
|
||||
val release = releaseProvider.fetchRelease(targetVersion).get
|
||||
if (release.isMarkedBroken) {
|
||||
if (globalCLIOptions.autoConfirm) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"The launcher release $targetVersion is marked as broken and it " +
|
||||
s"should not be used. Since `auto-confirm` is set, the upgrade " +
|
||||
s"will continue, but you may want to reconsider upgrading to a " +
|
||||
s"stable release."
|
||||
)
|
||||
} else {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"The launcher release $targetVersion is marked as broken and it " +
|
||||
s"should not be used."
|
||||
)
|
||||
@ -104,15 +108,15 @@ class LauncherUpgrader(
|
||||
val temporaryFiles =
|
||||
FileSystem.listDirectory(binRoot).filter(isTemporaryExecutable)
|
||||
if (temporaryFiles.nonEmpty && isStartup) {
|
||||
Logger.debug("Cleaning temporary files from a previous upgrade.")
|
||||
logger.debug("Cleaning temporary files from a previous upgrade.")
|
||||
}
|
||||
for (file <- temporaryFiles) {
|
||||
try {
|
||||
Files.delete(file)
|
||||
Logger.debug(s"Upgrade cleanup: removed `$file`.")
|
||||
logger.debug(s"Upgrade cleanup: removed `$file`.")
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
Logger.debug(s"Cannot remove temporary file $file: $e", e)
|
||||
logger.debug(s"Cannot remove temporary file $file: $e", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,7 +181,7 @@ class LauncherUpgrader(
|
||||
private def performStepByStepUpgrade(release: LauncherRelease): Unit = {
|
||||
val availableVersions = releaseProvider.fetchAllValidVersions().get
|
||||
val nextStepRelease = nextVersionToUpgradeTo(release, availableVersions)
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"Cannot upgrade to ${release.version} directly, " +
|
||||
s"so a multiple-step upgrade will be performed, first upgrading to " +
|
||||
s"${nextStepRelease.version}."
|
||||
@ -187,19 +191,19 @@ class LauncherUpgrader(
|
||||
"new." + nextStepRelease.version.toString
|
||||
)
|
||||
FileSystem.withTemporaryDirectory("enso-upgrade-step") { directory =>
|
||||
Logger.info(s"Downloading ${nextStepRelease.packageFileName}.")
|
||||
InfoLogger.info(s"Downloading ${nextStepRelease.packageFileName}.")
|
||||
val packagePath = directory / nextStepRelease.packageFileName
|
||||
nextStepRelease
|
||||
.downloadPackage(packagePath)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"Extracting the executable from ${nextStepRelease.packageFileName}."
|
||||
)
|
||||
extractExecutable(packagePath, temporaryExecutable)
|
||||
|
||||
Logger.info(
|
||||
InfoLogger.info(
|
||||
s"Upgraded to ${nextStepRelease.version}. " +
|
||||
s"Proceeding to the next step of the upgrade."
|
||||
)
|
||||
@ -222,7 +226,7 @@ class LauncherUpgrader(
|
||||
)
|
||||
}
|
||||
val nextRelease = releaseProvider.fetchRelease(minimumValidVersion).get
|
||||
Logger.debug(
|
||||
logger.debug(
|
||||
s"To upgrade to ${release.version}, " +
|
||||
s"the launcher will have to upgrade to ${nextRelease.version} first."
|
||||
)
|
||||
@ -293,7 +297,7 @@ class LauncherUpgrader(
|
||||
}
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
Logger.error(
|
||||
logger.error(
|
||||
"An error occurred when copying one of the non-crucial files and " +
|
||||
"directories. The upgrade will continue, but the README or " +
|
||||
"licences may be out of date.",
|
||||
@ -303,11 +307,11 @@ class LauncherUpgrader(
|
||||
|
||||
private def performUpgradeTo(release: LauncherRelease): Unit = {
|
||||
FileSystem.withTemporaryDirectory("enso-upgrade") { directory =>
|
||||
Logger.info(s"Downloading ${release.packageFileName}.")
|
||||
InfoLogger.info(s"Downloading ${release.packageFileName}.")
|
||||
val packagePath = directory / release.packageFileName
|
||||
release.downloadPackage(packagePath).waitForResult(showProgress).get
|
||||
|
||||
Logger.info("Extracting package.")
|
||||
InfoLogger.info("Extracting package.")
|
||||
Archive
|
||||
.extractArchive(packagePath, directory, None)
|
||||
.waitForResult(showProgress)
|
||||
@ -322,13 +326,13 @@ class LauncherUpgrader(
|
||||
|
||||
copyNonEssentialFiles(extractedRoot, release)
|
||||
|
||||
Logger.info("Replacing the old launcher executable with the new one.")
|
||||
InfoLogger.info("Replacing the old launcher executable with the new one.")
|
||||
replaceLauncherExecutable(temporaryExecutable)
|
||||
|
||||
val verb =
|
||||
if (release.version >= CurrentVersion.version) "upgraded"
|
||||
else "downgraded"
|
||||
Logger.info(s"Successfully $verb the launcher to ${release.version}.")
|
||||
InfoLogger.info(s"Successfully $verb the launcher to ${release.version}.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -346,7 +350,7 @@ class LauncherUpgrader(
|
||||
* one
|
||||
*/
|
||||
private def replaceLauncherExecutable(newExecutable: Path): Unit = {
|
||||
Logger.debug(s"Replacing $originalExecutable with $newExecutable")
|
||||
logger.debug(s"Replacing $originalExecutable with $newExecutable")
|
||||
if (OS.isWindows) {
|
||||
val oldName = temporaryExecutablePath(s"old-${CurrentVersion.version}")
|
||||
Files.move(originalExecutable, oldName)
|
||||
@ -415,16 +419,17 @@ object LauncherUpgrader {
|
||||
upgradeRequiredError: LauncherUpgradeRequiredError,
|
||||
originalArguments: Array[String]
|
||||
): Int = {
|
||||
val logger = Logger[LauncherUpgrader].enter("auto-upgrade")
|
||||
val autoConfirm = upgradeRequiredError.globalCLIOptions.autoConfirm
|
||||
def shouldProceed: Boolean =
|
||||
if (autoConfirm) {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
"A more recent launcher version is required. Since `auto-confirm` " +
|
||||
"is set, the launcher upgrade will be peformed automatically."
|
||||
)
|
||||
true
|
||||
} else {
|
||||
Logger.warn(
|
||||
logger.warn(
|
||||
s"A more recent launcher version (at least " +
|
||||
s"${upgradeRequiredError.expectedLauncherVersion}) is required to " +
|
||||
s"continue."
|
||||
@ -445,19 +450,21 @@ object LauncherUpgrader {
|
||||
try {
|
||||
upgrader.upgrade(targetVersion)
|
||||
|
||||
Logger.info("Re-running the current command with the upgraded launcher.")
|
||||
InfoLogger.info(
|
||||
"Re-running the current command with the upgraded launcher."
|
||||
)
|
||||
|
||||
val arguments =
|
||||
InternalOpts.removeInternalTestOptions(originalArguments.toIndexedSeq)
|
||||
val rerunCommand =
|
||||
Seq(launcherExecutable.toAbsolutePath.normalize.toString) ++ arguments
|
||||
Logger.debug(s"Running `${rerunCommand.mkString(" ")}`.")
|
||||
logger.debug(s"Running `${rerunCommand.mkString(" ")}`.")
|
||||
val processBuilder = new ProcessBuilder(rerunCommand: _*)
|
||||
val process = processBuilder.inheritIO().start()
|
||||
process.waitFor()
|
||||
} catch {
|
||||
case _: AnotherUpgradeInProgressError =>
|
||||
Logger.error(
|
||||
logger.error(
|
||||
"Another upgrade is in progress." +
|
||||
"Please wait for it to finish and manually re-run the requested " +
|
||||
"command."
|
||||
|
@ -25,12 +25,10 @@ class DefaultVersionSpec extends ComponentsManagerTest {
|
||||
}
|
||||
|
||||
"fallback to latest installed version" in {
|
||||
Logger.suppressWarnings {
|
||||
val (componentsManager, configManager) =
|
||||
makeConfigAndComponentsManagers()
|
||||
componentsManager.findOrInstallEngine(SemVer(0, 0, 0))
|
||||
configManager.defaultVersion shouldEqual SemVer(0, 0, 0)
|
||||
}
|
||||
val (componentsManager, configManager) =
|
||||
makeConfigAndComponentsManagers()
|
||||
componentsManager.findOrInstallEngine(SemVer(0, 0, 0))
|
||||
configManager.defaultVersion shouldEqual SemVer(0, 0, 0)
|
||||
}
|
||||
|
||||
"set an exact version in the config" in {
|
||||
|
@ -0,0 +1,11 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import org.enso.loggingservice.TestLogger
|
||||
import org.scalatest.{BeforeAndAfterAll, Suite}
|
||||
|
||||
trait DropLogs extends BeforeAndAfterAll { self: Suite =>
|
||||
override protected def afterAll(): Unit = {
|
||||
super.afterAll()
|
||||
TestLogger.dropLogs()
|
||||
}
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.io.{BufferedReader, InputStream, InputStreamReader}
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.io.{BufferedReader, IOException, InputStream, InputStreamReader}
|
||||
import java.lang.{ProcessBuilder => JProcessBuilder}
|
||||
import java.util.concurrent.{Semaphore, TimeUnit, TimeoutException}
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.util.concurrent.{Semaphore, TimeUnit}
|
||||
|
||||
import org.scalatest.concurrent.{Signaler, TimeLimitedTests}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.matchers.{MatchResult, Matcher}
|
||||
import org.scalatest.time.Span
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import org.scalatest.time.SpanSugar._
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.collection.Factory
|
||||
import scala.concurrent.TimeoutException
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.jdk.StreamConverters._
|
||||
|
||||
@ -222,9 +223,15 @@ trait NativeTest extends AnyWordSpec with Matchers with TimeLimitedTests {
|
||||
case StdErr => errQueue
|
||||
case StdOut => outQueue
|
||||
}
|
||||
while ({ line = reader.readLine(); line != null }) {
|
||||
queue.add(line)
|
||||
ioHandlers.foreach(f => f(line, streamType))
|
||||
try {
|
||||
while ({ line = reader.readLine(); line != null }) {
|
||||
queue.add(line)
|
||||
ioHandlers.foreach(f => f(line, streamType))
|
||||
}
|
||||
} catch {
|
||||
case _: InterruptedException =>
|
||||
case _: IOException =>
|
||||
ioHandlers.foreach(f => f("<Unexpected EOF>", streamType))
|
||||
}
|
||||
}
|
||||
|
||||
@ -255,9 +262,11 @@ trait NativeTest extends AnyWordSpec with Matchers with TimeLimitedTests {
|
||||
ioHandlers ++= Seq(handler _)
|
||||
}
|
||||
|
||||
errQueue.asScala.toSeq.foreach(handler(_, StdErr))
|
||||
|
||||
val acquired = semaphore.tryAcquire(timeoutSeconds, TimeUnit.SECONDS)
|
||||
if (!acquired) {
|
||||
throw new RuntimeException(s"Waiting for `$message` timed out.")
|
||||
throw new TimeoutException(s"Waiting for `$message` timed out.")
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,6 +298,13 @@ trait NativeTest extends AnyWordSpec with Matchers with TimeLimitedTests {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to kill the process immediately.
|
||||
*/
|
||||
def kill(): Unit = {
|
||||
process.destroyForcibly()
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the process to finish and returns its [[RunResult]].
|
||||
*
|
||||
@ -301,7 +317,7 @@ trait NativeTest extends AnyWordSpec with Matchers with TimeLimitedTests {
|
||||
*/
|
||||
def join(
|
||||
waitForDescendants: Boolean = true,
|
||||
timeoutSeconds: Long = 10
|
||||
timeoutSeconds: Long = 15
|
||||
): RunResult = {
|
||||
var descendants: Seq[ProcessHandle] = Seq()
|
||||
try {
|
||||
|
@ -17,7 +17,7 @@ trait WithTemporaryDirectory extends Suite with BeforeAndAfterEach {
|
||||
*/
|
||||
override def beforeEach(): Unit = {
|
||||
super.beforeEach()
|
||||
testDirectory = Files.createTempDirectory("enso-test")
|
||||
prepareTemporaryDirectory()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,7 +41,7 @@ trait WithTemporaryDirectory extends Suite with BeforeAndAfterEach {
|
||||
* because if the test runs other executables, they may take a moment to
|
||||
* terminate even after the test completed).
|
||||
*/
|
||||
private def robustDeleteDirectory(dir: File): Unit = {
|
||||
def robustDeleteDirectory(dir: File): Unit = {
|
||||
def tryRemoving(retry: Int): Unit = {
|
||||
try {
|
||||
FileUtils.deleteDirectory(dir)
|
||||
@ -58,4 +58,20 @@ trait WithTemporaryDirectory extends Suite with BeforeAndAfterEach {
|
||||
|
||||
tryRemoving(30)
|
||||
}
|
||||
|
||||
private def prepareTemporaryDirectory(): Unit = {
|
||||
testDirectory = Files.createTempDirectory("enso-test")
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the temporary directory with a fresh one so that the test can be
|
||||
* safely retried.
|
||||
*
|
||||
* Without this, retried tests re-use the directory which may cause problems.
|
||||
*/
|
||||
def allowForRetry(action: => Unit): Unit = {
|
||||
robustDeleteDirectory(testDirectory.toFile)
|
||||
prepareTemporaryDirectory()
|
||||
action
|
||||
}
|
||||
}
|
||||
|
@ -1,158 +1,146 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.config.GlobalConfigurationManager
|
||||
import org.enso.loggingservice.{LogLevel, TestLogger}
|
||||
|
||||
class ComponentsManagerSpec extends ComponentsManagerTest {
|
||||
|
||||
"ComponentsManager" should {
|
||||
"find the latest engine version in semver ordering " +
|
||||
"(skipping broken releases)" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
componentsManager.fetchLatestEngineVersion() shouldEqual SemVer(0, 0, 1)
|
||||
}
|
||||
val componentsManager = makeComponentsManager()
|
||||
componentsManager.fetchLatestEngineVersion() shouldEqual SemVer(0, 0, 1)
|
||||
}
|
||||
|
||||
"install the engine and a matching runtime for it" in {
|
||||
Logger.suppressWarnings {
|
||||
val (distributionManager, componentsManager, _) = makeManagers()
|
||||
val (distributionManager, componentsManager, _) = makeManagers()
|
||||
|
||||
val version = SemVer(0, 0, 1)
|
||||
val engine = componentsManager.findOrInstallEngine(SemVer(0, 0, 1))
|
||||
val version = SemVer(0, 0, 1)
|
||||
val engine = componentsManager.findOrInstallEngine(SemVer(0, 0, 1))
|
||||
|
||||
engine.version shouldEqual version
|
||||
assert(
|
||||
engine.path.startsWith(distributionManager.paths.engines),
|
||||
"Engine should be installed in the engines directory."
|
||||
)
|
||||
engine.version shouldEqual version
|
||||
assert(
|
||||
engine.path.startsWith(distributionManager.paths.engines),
|
||||
"Engine should be installed in the engines directory."
|
||||
)
|
||||
|
||||
val runtime = componentsManager.findRuntime(engine)
|
||||
runtime.value.version shouldEqual RuntimeVersion(SemVer(2, 0, 0), "11")
|
||||
assert(
|
||||
runtime.value.path.startsWith(distributionManager.paths.runtimes),
|
||||
"Engine should be installed in the engines directory."
|
||||
)
|
||||
}
|
||||
val runtime = componentsManager.findRuntime(engine)
|
||||
runtime.value.version shouldEqual RuntimeVersion(SemVer(2, 0, 0), "11")
|
||||
assert(
|
||||
runtime.value.path.startsWith(distributionManager.paths.runtimes),
|
||||
"Engine should be installed in the engines directory."
|
||||
)
|
||||
}
|
||||
|
||||
"list installed engines and runtimes" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val engineVersions =
|
||||
Set(SemVer(0, 0, 0), SemVer(0, 0, 1), SemVer(0, 0, 1, Some("pre")))
|
||||
val runtimeVersions =
|
||||
Set(
|
||||
RuntimeVersion(SemVer(1, 0, 0), "11"),
|
||||
RuntimeVersion(SemVer(2, 0, 0), "11")
|
||||
)
|
||||
engineVersions.map(
|
||||
componentsManager.findOrInstallEngine(_, complain = false)
|
||||
val componentsManager = makeComponentsManager()
|
||||
val engineVersions =
|
||||
Set(SemVer(0, 0, 0), SemVer(0, 0, 1), SemVer(0, 0, 1, Some("pre")))
|
||||
val runtimeVersions =
|
||||
Set(
|
||||
RuntimeVersion(SemVer(1, 0, 0), "11"),
|
||||
RuntimeVersion(SemVer(2, 0, 0), "11")
|
||||
)
|
||||
engineVersions.map(
|
||||
componentsManager.findOrInstallEngine(_, complain = false)
|
||||
)
|
||||
|
||||
componentsManager
|
||||
.listInstalledEngines()
|
||||
.map(_.version)
|
||||
.toSet shouldEqual engineVersions
|
||||
componentsManager
|
||||
.listInstalledRuntimes()
|
||||
.map(_.version)
|
||||
.toSet shouldEqual runtimeVersions
|
||||
componentsManager
|
||||
.listInstalledEngines()
|
||||
.map(_.version)
|
||||
.toSet shouldEqual engineVersions
|
||||
componentsManager
|
||||
.listInstalledRuntimes()
|
||||
.map(_.version)
|
||||
.toSet shouldEqual runtimeVersions
|
||||
|
||||
val runtime2 =
|
||||
componentsManager
|
||||
.findRuntime(RuntimeVersion(SemVer(2, 0, 0), "11"))
|
||||
.value
|
||||
componentsManager.findEnginesUsingRuntime(runtime2) should have length 2
|
||||
}
|
||||
val runtime2 =
|
||||
componentsManager
|
||||
.findRuntime(RuntimeVersion(SemVer(2, 0, 0), "11"))
|
||||
.value
|
||||
componentsManager.findEnginesUsingRuntime(runtime2) should have length 2
|
||||
}
|
||||
|
||||
"preserve the broken mark when installing a broken release" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val brokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(
|
||||
brokenVersion,
|
||||
complain = false
|
||||
)
|
||||
val componentsManager = makeComponentsManager()
|
||||
val brokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(
|
||||
brokenVersion,
|
||||
complain = false
|
||||
)
|
||||
|
||||
assert(
|
||||
componentsManager.findEngine(brokenVersion).value.isMarkedBroken,
|
||||
"The broken release should still be marked as broken after being " +
|
||||
"installed and loaded."
|
||||
)
|
||||
}
|
||||
assert(
|
||||
componentsManager.findEngine(brokenVersion).value.isMarkedBroken,
|
||||
"The broken release should still be marked as broken after being " +
|
||||
"installed and loaded."
|
||||
)
|
||||
}
|
||||
|
||||
"skip broken releases when finding latest installed version" in {
|
||||
Logger.suppressWarnings {
|
||||
val (distributionManager, componentsManager, _) = makeManagers()
|
||||
val configurationManager =
|
||||
new GlobalConfigurationManager(componentsManager, distributionManager)
|
||||
val (distributionManager, componentsManager, _) = makeManagers()
|
||||
val configurationManager =
|
||||
new GlobalConfigurationManager(componentsManager, distributionManager)
|
||||
|
||||
val validVersion = SemVer(0, 0, 1)
|
||||
val newerButBrokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(validVersion)
|
||||
componentsManager.findOrInstallEngine(newerButBrokenVersion)
|
||||
val validVersion = SemVer(0, 0, 1)
|
||||
val newerButBrokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(validVersion)
|
||||
componentsManager.findOrInstallEngine(newerButBrokenVersion)
|
||||
|
||||
configurationManager.defaultVersion shouldEqual validVersion
|
||||
}
|
||||
configurationManager.defaultVersion shouldEqual validVersion
|
||||
}
|
||||
|
||||
"issue a warning when a broken release is requested" in {
|
||||
val stream = new java.io.ByteArrayOutputStream()
|
||||
Console.withErr(stream) {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val componentsManager = makeComponentsManager()
|
||||
|
||||
val brokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(brokenVersion)
|
||||
|
||||
componentsManager.findEngine(brokenVersion).value
|
||||
val brokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
val logs = TestLogger.gatherLogs {
|
||||
componentsManager.findOrInstallEngine(brokenVersion, complain = false)
|
||||
}
|
||||
val stderr = stream.toString
|
||||
stderr should include("is marked as broken")
|
||||
stderr should include("consider upgrading")
|
||||
val warnings = logs.filter(_.logLevel == LogLevel.Warning)
|
||||
warnings should have size 1
|
||||
val expectedWarning = warnings.head.message
|
||||
expectedWarning should include("is marked as broken")
|
||||
expectedWarning should include("consider changing")
|
||||
componentsManager.findEngine(brokenVersion).value
|
||||
}
|
||||
|
||||
"uninstall the runtime iff it is not used by any engines" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val engineVersions =
|
||||
Seq(SemVer(0, 0, 0), SemVer(0, 0, 1), SemVer(0, 0, 1, Some("pre")))
|
||||
engineVersions.map(
|
||||
componentsManager.findOrInstallEngine(_, complain = false)
|
||||
)
|
||||
val componentsManager = makeComponentsManager()
|
||||
val engineVersions =
|
||||
Seq(SemVer(0, 0, 0), SemVer(0, 0, 1), SemVer(0, 0, 1, Some("pre")))
|
||||
engineVersions.map(
|
||||
componentsManager.findOrInstallEngine(_, complain = false)
|
||||
)
|
||||
|
||||
componentsManager.listInstalledEngines() should have length 3
|
||||
componentsManager.listInstalledRuntimes() should have length 2
|
||||
componentsManager.listInstalledEngines() should have length 3
|
||||
componentsManager.listInstalledRuntimes() should have length 2
|
||||
|
||||
// remove the engine that shares the runtime with another one
|
||||
val version1 = SemVer(0, 0, 1, Some("pre"))
|
||||
componentsManager.uninstallEngine(version1)
|
||||
val engines1 = componentsManager.listInstalledEngines()
|
||||
engines1 should have length 2
|
||||
engines1.map(_.version) should not contain version1
|
||||
componentsManager.listInstalledRuntimes() should have length 2
|
||||
// remove the engine that shares the runtime with another one
|
||||
val version1 = SemVer(0, 0, 1, Some("pre"))
|
||||
componentsManager.uninstallEngine(version1)
|
||||
val engines1 = componentsManager.listInstalledEngines()
|
||||
engines1 should have length 2
|
||||
engines1.map(_.version) should not contain version1
|
||||
componentsManager.listInstalledRuntimes() should have length 2
|
||||
|
||||
// remove the second engine that shared the runtime
|
||||
val version2 = SemVer(0, 0, 1)
|
||||
componentsManager.uninstallEngine(version2)
|
||||
val engines2 = componentsManager.listInstalledEngines()
|
||||
engines2 should have length 1
|
||||
engines2.map(_.version) should not contain version2
|
||||
val runtimes2 = componentsManager.listInstalledRuntimes()
|
||||
runtimes2 should have length 1
|
||||
runtimes2.map(_.version).head shouldEqual RuntimeVersion(
|
||||
SemVer(1, 0, 0),
|
||||
"11"
|
||||
)
|
||||
// remove the second engine that shared the runtime
|
||||
val version2 = SemVer(0, 0, 1)
|
||||
componentsManager.uninstallEngine(version2)
|
||||
val engines2 = componentsManager.listInstalledEngines()
|
||||
engines2 should have length 1
|
||||
engines2.map(_.version) should not contain version2
|
||||
val runtimes2 = componentsManager.listInstalledRuntimes()
|
||||
runtimes2 should have length 1
|
||||
runtimes2.map(_.version).head shouldEqual RuntimeVersion(
|
||||
SemVer(1, 0, 0),
|
||||
"11"
|
||||
)
|
||||
|
||||
// remove the last engine
|
||||
componentsManager.uninstallEngine(SemVer(0, 0, 0))
|
||||
componentsManager.listInstalledEngines() should have length 0
|
||||
componentsManager.listInstalledRuntimes() should have length 0
|
||||
}
|
||||
// remove the last engine
|
||||
componentsManager.uninstallEngine(SemVer(0, 0, 0))
|
||||
componentsManager.listInstalledEngines() should have length 0
|
||||
componentsManager.listInstalledRuntimes() should have length 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,18 @@ package org.enso.launcher.components
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.cli.{ColorMode, GlobalCLIOptions}
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.locking.TestLocalResourceManager
|
||||
import org.enso.launcher.releases.engine.EngineReleaseProvider
|
||||
import org.enso.launcher.releases.runtime.GraalCEReleaseProvider
|
||||
import org.enso.launcher.releases.testing.FakeReleaseProvider
|
||||
import org.enso.launcher.{Environment, FakeEnvironment, WithTemporaryDirectory}
|
||||
import org.enso.launcher.{
|
||||
DropLogs,
|
||||
Environment,
|
||||
FakeEnvironment,
|
||||
WithTemporaryDirectory
|
||||
}
|
||||
import org.enso.pkg.{PackageManager, SemVerEnsoVersion}
|
||||
import org.scalatest.OptionValues
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@ -20,7 +25,8 @@ class ComponentsManagerTest
|
||||
with Matchers
|
||||
with OptionValues
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment {
|
||||
with FakeEnvironment
|
||||
with DropLogs {
|
||||
|
||||
/**
|
||||
* Creates the [[DistributionManager]], [[ComponentsManager]] and an
|
||||
@ -57,7 +63,8 @@ class ComponentsManagerTest
|
||||
GlobalCLIOptions(
|
||||
autoConfirm = true,
|
||||
hideProgress = true,
|
||||
useJSON = false
|
||||
useJSON = false,
|
||||
colorMode = ColorMode.Never
|
||||
),
|
||||
distributionManager,
|
||||
TestLocalResourceManager.create(),
|
||||
|
@ -3,16 +3,21 @@ package org.enso.launcher.components.runner
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.util.UUID
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.components.ComponentsManagerTest
|
||||
import org.enso.launcher.config.GlobalConfigurationManager
|
||||
import org.enso.launcher.project.ProjectManager
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class RunnerSpec extends ComponentsManagerTest {
|
||||
private val defaultEngineVersion = SemVer(0, 0, 0, Some("default"))
|
||||
|
||||
private val fakeUri = Uri("ws://test:1234/")
|
||||
|
||||
def makeFakeRunner(
|
||||
cwdOverride: Option[Path] = None,
|
||||
extraEnv: Map[String, String] = Map.empty
|
||||
@ -25,7 +30,13 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
val projectManager = new ProjectManager(configurationManager)
|
||||
val cwd = cwdOverride.getOrElse(getTestDirectory)
|
||||
val runner =
|
||||
new Runner(projectManager, configurationManager, componentsManager, env) {
|
||||
new Runner(
|
||||
projectManager,
|
||||
configurationManager,
|
||||
componentsManager,
|
||||
env,
|
||||
Future.successful(Some(fakeUri))
|
||||
) {
|
||||
override protected val currentWorkingDirectory: Path = cwd
|
||||
}
|
||||
runner
|
||||
@ -33,57 +44,64 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
|
||||
"Runner" should {
|
||||
"create a command from settings" in {
|
||||
Logger.suppressWarnings {
|
||||
val envOptions = "-Xfrom-env -Denv=env"
|
||||
val runner =
|
||||
makeFakeRunner(extraEnv = Map("ENSO_JVM_OPTS" -> envOptions))
|
||||
val envOptions = "-Xfrom-env -Denv=env"
|
||||
val runner =
|
||||
makeFakeRunner(extraEnv = Map("ENSO_JVM_OPTS" -> envOptions))
|
||||
|
||||
val runSettings = RunSettings(SemVer(0, 0, 0), Seq("arg1", "--flag2"))
|
||||
val jvmOptions = Seq(("locally-added-options", "value1"))
|
||||
val runSettings = RunSettings(
|
||||
SemVer(0, 0, 0),
|
||||
Seq("arg1", "--flag2"),
|
||||
connectLoggerIfAvailable = true
|
||||
)
|
||||
val jvmOptions = Seq(("locally-added-options", "value1"))
|
||||
|
||||
val enginePath =
|
||||
getTestDirectory / "test_data" / "dist" / "0.0.0"
|
||||
val runtimePath =
|
||||
(enginePath / "component" / "runtime.jar").toAbsolutePath.normalize
|
||||
val runnerPath =
|
||||
(enginePath / "component" / "runner.jar").toAbsolutePath.normalize
|
||||
val enginePath =
|
||||
getTestDirectory / "test_data" / "dist" / "0.0.0"
|
||||
val runtimePath =
|
||||
(enginePath / "component" / "runtime.jar").toAbsolutePath.normalize
|
||||
val runnerPath =
|
||||
(enginePath / "component" / "runner.jar").toAbsolutePath.normalize
|
||||
|
||||
def checkCommandLine(command: Command): Unit = {
|
||||
val commandLine = command.command.mkString(" ")
|
||||
val arguments = command.command.tail
|
||||
arguments should contain("-Xfrom-env")
|
||||
arguments should contain("-Denv=env")
|
||||
arguments should contain("-Dlocally-added-options=value1")
|
||||
arguments should contain("-Dlocally-added-options=value1")
|
||||
arguments should contain("-Doptions-added-from-manifest=42")
|
||||
arguments should contain("-Xanother-one")
|
||||
commandLine should endWith("arg1 --flag2")
|
||||
def checkCommandLine(command: Command): Unit = {
|
||||
val arguments = command.command.tail
|
||||
val javaArguments = arguments.takeWhile(_ != "-jar")
|
||||
val appArguments = arguments.dropWhile(_ != runnerPath.toString).tail
|
||||
javaArguments should contain("-Xfrom-env")
|
||||
javaArguments should contain("-Denv=env")
|
||||
javaArguments should contain("-Dlocally-added-options=value1")
|
||||
javaArguments should contain("-Dlocally-added-options=value1")
|
||||
javaArguments should contain("-Doptions-added-from-manifest=42")
|
||||
javaArguments should contain("-Xanother-one")
|
||||
|
||||
arguments should contain(s"-Dtruffle.class.path.append=$runtimePath")
|
||||
arguments.filter(
|
||||
_.contains("truffle.class.path.append")
|
||||
) should have length 1
|
||||
javaArguments should contain(
|
||||
s"-Dtruffle.class.path.append=$runtimePath"
|
||||
)
|
||||
javaArguments.filter(
|
||||
_.contains("truffle.class.path.append")
|
||||
) should have length 1
|
||||
|
||||
commandLine should include(s"-jar $runnerPath")
|
||||
}
|
||||
val appCommandLine = appArguments.mkString(" ")
|
||||
|
||||
runner.withCommand(
|
||||
runSettings,
|
||||
JVMSettings(useSystemJVM = true, jvmOptions = jvmOptions)
|
||||
) { systemCommand =>
|
||||
systemCommand.command.head shouldEqual "java"
|
||||
checkCommandLine(systemCommand)
|
||||
}
|
||||
appCommandLine shouldEqual s"--logger-connect $fakeUri arg1 --flag2"
|
||||
command.command.mkString(" ") should include(s"-jar $runnerPath")
|
||||
}
|
||||
|
||||
runner.withCommand(
|
||||
runSettings,
|
||||
JVMSettings(useSystemJVM = false, jvmOptions = jvmOptions)
|
||||
) { managedCommand =>
|
||||
managedCommand.command.head should include("java")
|
||||
val javaHome =
|
||||
managedCommand.extraEnv.find(_._1 == "JAVA_HOME").value._2
|
||||
javaHome should include("graalvm-ce")
|
||||
}
|
||||
runner.withCommand(
|
||||
runSettings,
|
||||
JVMSettings(useSystemJVM = true, jvmOptions = jvmOptions)
|
||||
) { systemCommand =>
|
||||
systemCommand.command.head shouldEqual "java"
|
||||
checkCommandLine(systemCommand)
|
||||
}
|
||||
|
||||
runner.withCommand(
|
||||
runSettings,
|
||||
JVMSettings(useSystemJVM = false, jvmOptions = jvmOptions)
|
||||
) { managedCommand =>
|
||||
managedCommand.command.head should include("java")
|
||||
val javaHome =
|
||||
managedCommand.extraEnv.find(_._1 == "JAVA_HOME").value._2
|
||||
javaHome should include("graalvm-ce")
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,7 +140,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.repl(
|
||||
projectPath = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq("arg", "--flag")
|
||||
additionalArguments = Seq("arg", "--flag"),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -146,7 +165,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.repl(
|
||||
projectPath = Some(projectPath),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -159,7 +179,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.repl(
|
||||
projectPath = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -172,7 +193,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.repl(
|
||||
projectPath = Some(projectPath),
|
||||
versionOverride = Some(overridden),
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -199,7 +221,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.languageServer(
|
||||
options,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq("additional")
|
||||
additionalArguments = Seq("additional"),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -218,7 +241,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.languageServer(
|
||||
options,
|
||||
versionOverride = Some(overridden),
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
.version shouldEqual overridden
|
||||
@ -236,7 +260,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.run(
|
||||
path = Some(projectPath),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -249,7 +274,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.run(
|
||||
path = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -262,7 +288,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.run(
|
||||
path = Some(projectPath),
|
||||
versionOverride = Some(overridden),
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -275,7 +302,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.run(
|
||||
path = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.isFailure,
|
||||
"Running outside project without providing any paths should be an error"
|
||||
@ -300,7 +328,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.run(
|
||||
path = Some(outsideFile),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
@ -323,7 +352,8 @@ class RunnerSpec extends ComponentsManagerTest {
|
||||
.run(
|
||||
path = Some(insideFile),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
additionalArguments = Seq(),
|
||||
logLevel = LogLevel.Info
|
||||
)
|
||||
.get
|
||||
|
||||
|
@ -2,7 +2,7 @@ package org.enso.launcher.config
|
||||
|
||||
import io.circe.Json
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.{FakeEnvironment, WithTemporaryDirectory}
|
||||
import org.enso.launcher.{DropLogs, FakeEnvironment, WithTemporaryDirectory}
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.locking.TestLocalResourceManager
|
||||
import org.scalatest.OptionValues
|
||||
@ -14,7 +14,8 @@ class GlobalConfigurationManagerSpec
|
||||
with Matchers
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment
|
||||
with OptionValues {
|
||||
with OptionValues
|
||||
with DropLogs {
|
||||
def makeConfigManager(): GlobalConfigurationManager = {
|
||||
val env = fakeInstalledEnvironment()
|
||||
val distributionManager =
|
||||
|
@ -4,7 +4,12 @@ import java.nio.file.Path
|
||||
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.locking.TestLocalResourceManager
|
||||
import org.enso.launcher.{Environment, FakeEnvironment, WithTemporaryDirectory}
|
||||
import org.enso.launcher.{
|
||||
DropLogs,
|
||||
Environment,
|
||||
FakeEnvironment,
|
||||
WithTemporaryDirectory
|
||||
}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
@ -12,7 +17,8 @@ class DistributionManagerSpec
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment {
|
||||
with FakeEnvironment
|
||||
with DropLogs {
|
||||
|
||||
"DistributionManager" should {
|
||||
"detect portable distribution" in {
|
||||
|
@ -28,6 +28,7 @@ class UninstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
else getTestDirectory / "enso-config"
|
||||
val dataDirectory = installedRoot
|
||||
val runDirectory = installedRoot
|
||||
val logDirectory = installedRoot / "log"
|
||||
val portableLauncher = binDirectory / OS.executableName("enso")
|
||||
copyLauncherTo(portableLauncher)
|
||||
Files.createDirectories(dataDirectory / "dist")
|
||||
@ -43,7 +44,8 @@ class UninstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
"ENSO_DATA_DIRECTORY" -> dataDirectory.toString,
|
||||
"ENSO_BIN_DIRECTORY" -> binDirectory.toString,
|
||||
"ENSO_CONFIG_DIRECTORY" -> configDirectory.toString,
|
||||
"ENSO_RUNTIME_DIRECTORY" -> runDirectory.toString
|
||||
"ENSO_RUNTIME_DIRECTORY" -> runDirectory.toString,
|
||||
"ENSO_LOG_DIRECTORY" -> logDirectory.toString
|
||||
)
|
||||
(portableLauncher, env)
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ import java.nio.file.{Files, Path}
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.cli.{ColorMode, GlobalCLIOptions}
|
||||
import org.enso.launcher.components.{ComponentsManager, RuntimeVersion}
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.releases.engine.{EngineRelease, EngineReleaseProvider}
|
||||
@ -12,11 +13,11 @@ import org.enso.launcher.releases.runtime.GraalCEReleaseProvider
|
||||
import org.enso.launcher.releases.testing.FakeReleaseProvider
|
||||
import org.enso.launcher.{
|
||||
components,
|
||||
DropLogs,
|
||||
FakeEnvironment,
|
||||
FileSystem,
|
||||
WithTemporaryDirectory
|
||||
}
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.scalatest.BeforeAndAfterEach
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
@ -28,7 +29,8 @@ class ConcurrencyTest
|
||||
with Matchers
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment
|
||||
with BeforeAndAfterEach {
|
||||
with BeforeAndAfterEach
|
||||
with DropLogs {
|
||||
|
||||
case class WrapEngineRelease(
|
||||
originalRelease: EngineRelease,
|
||||
@ -131,7 +133,8 @@ class ConcurrencyTest
|
||||
GlobalCLIOptions(
|
||||
autoConfirm = true,
|
||||
hideProgress = true,
|
||||
useJSON = false
|
||||
useJSON = false,
|
||||
colorMode = ColorMode.Never
|
||||
),
|
||||
distributionManager,
|
||||
resourceManager,
|
||||
|
@ -103,7 +103,12 @@ class TestSynchronizer {
|
||||
def summarizeReports(): Seq[String] = reports.asScala.toSeq
|
||||
|
||||
private def reportException(thread: Thread, throwable: Throwable): Unit = {
|
||||
System.err.println(s"${thread.getName} got an exception: $throwable")
|
||||
throwable match {
|
||||
case _: InterruptedException =>
|
||||
System.err.println(s"${thread.getName} was interrupted: $throwable")
|
||||
case _ =>
|
||||
System.err.println(s"${thread.getName} got an exception: $throwable")
|
||||
}
|
||||
throwable.printStackTrace()
|
||||
hadException = true
|
||||
}
|
||||
@ -118,13 +123,22 @@ class TestSynchronizer {
|
||||
*/
|
||||
def join(joinTimeOutSeconds: Long = timeOutSeconds): Unit = {
|
||||
var hadTimeout = false
|
||||
for (thread <- threads.reverse) {
|
||||
thread.join(joinTimeOutSeconds * 1000)
|
||||
if (thread.isAlive) {
|
||||
System.err.println(s"Thread ${thread.getName} timed out.")
|
||||
thread.interrupt()
|
||||
hadTimeout = true
|
||||
try {
|
||||
for (thread <- threads.reverse) {
|
||||
thread.join(joinTimeOutSeconds * 1000)
|
||||
if (thread.isAlive) {
|
||||
System.err.println(s"Thread ${thread.getName} timed out.")
|
||||
thread.interrupt()
|
||||
hadTimeout = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case interrupt: InterruptedException =>
|
||||
for (thread <- threads) {
|
||||
try thread.interrupt()
|
||||
catch { case _: Throwable => }
|
||||
}
|
||||
throw interrupt
|
||||
}
|
||||
|
||||
if (hadException) {
|
||||
|
@ -10,6 +10,8 @@ import org.enso.launcher.locking.{FileLockManager, LockType}
|
||||
import org.scalatest.exceptions.TestFailedException
|
||||
import org.scalatest.{BeforeAndAfterAll, OptionValues}
|
||||
|
||||
import scala.concurrent.TimeoutException
|
||||
|
||||
class UpgradeSpec
|
||||
extends NativeTest
|
||||
with WithTemporaryDirectory
|
||||
@ -90,7 +92,11 @@ class UpgradeSpec
|
||||
val sourceLauncherLocation =
|
||||
launcherVersion.map(builtLauncherBinary).getOrElse(baseLauncherLocation)
|
||||
Files.createDirectories(launcherPath.getParent)
|
||||
Files.copy(sourceLauncherLocation, launcherPath)
|
||||
Files.copy(
|
||||
sourceLauncherLocation,
|
||||
launcherPath,
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
if (portable) {
|
||||
val root = launcherPath.getParent.getParent
|
||||
FileSystem.writeTextFile(root / ".enso.portable", "mark")
|
||||
@ -233,10 +239,7 @@ class UpgradeSpec
|
||||
)
|
||||
|
||||
checkVersion() shouldEqual SemVer(0, 0, 0)
|
||||
// run(Seq("upgrade", "0.0.3")) should returnSuccess
|
||||
val proc = startLauncher(Seq("upgrade", "0.0.3"))
|
||||
proc.printIO()
|
||||
proc.join(timeoutSeconds = 20) should returnSuccess
|
||||
run(Seq("upgrade", "0.0.3")) should returnSuccess
|
||||
|
||||
checkVersion() shouldEqual SemVer(0, 0, 3)
|
||||
|
||||
@ -327,7 +330,12 @@ class UpgradeSpec
|
||||
)
|
||||
|
||||
val firstSuspended = startLauncher(
|
||||
Seq("upgrade", "0.0.2", "--internal-emulate-repository-wait")
|
||||
Seq(
|
||||
"upgrade",
|
||||
"0.0.2",
|
||||
"--internal-emulate-repository-wait",
|
||||
"--launcher-log-level=trace"
|
||||
)
|
||||
)
|
||||
try {
|
||||
firstSuspended.waitForMessageOnErrorStream(
|
||||
@ -339,6 +347,15 @@ class UpgradeSpec
|
||||
|
||||
secondFailed.stderr should include("Another upgrade is in progress")
|
||||
secondFailed.exitCode shouldEqual 1
|
||||
} catch {
|
||||
case e: TimeoutException =>
|
||||
System.err.println(
|
||||
"Waiting for the lock timed out, " +
|
||||
"the process had the following output:"
|
||||
)
|
||||
firstSuspended.printIO()
|
||||
firstSuspended.kill()
|
||||
throw e
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package org.enso.runner
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
import org.enso.loggingservice.{JavaLoggingLogHandler, LogLevel}
|
||||
import org.enso.polyglot.debugger.{
|
||||
DebugServerInfo,
|
||||
DebuggerSessionManagerEndpoint
|
||||
@ -22,6 +23,8 @@ class ContextFactory {
|
||||
* @param in the input stream for standard in
|
||||
* @param out the output stream for standard out
|
||||
* @param repl the Repl manager to use for this context
|
||||
* @param logLevel the log level for this context
|
||||
* @param strictErrors whether or not to use strict errors
|
||||
* @return configured Context instance
|
||||
*/
|
||||
def create(
|
||||
@ -29,6 +32,7 @@ class ContextFactory {
|
||||
in: InputStream,
|
||||
out: OutputStream,
|
||||
repl: Repl,
|
||||
logLevel: LogLevel,
|
||||
strictErrors: Boolean = false
|
||||
): PolyglotContext = {
|
||||
val context = Context
|
||||
@ -45,6 +49,13 @@ class ContextFactory {
|
||||
new DebuggerSessionManagerEndpoint(repl, peer)
|
||||
} else null
|
||||
}
|
||||
.option(
|
||||
RuntimeOptions.LOG_LEVEL,
|
||||
JavaLoggingLogHandler.getJavaLogLevelFor(logLevel).getName
|
||||
)
|
||||
.logHandler(
|
||||
JavaLoggingLogHandler.create(JavaLoggingLogHandler.defaultLevelMapping)
|
||||
)
|
||||
.build
|
||||
new PolyglotContext(context)
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ package org.enso.runner
|
||||
import java.io.File
|
||||
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
|
||||
import org.enso.loggingservice.LogLevel
|
||||
import org.enso.pkg.{Contact, PackageManager, SemVerEnsoVersion}
|
||||
import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext}
|
||||
import org.enso.version.VersionDescription
|
||||
@ -35,6 +37,8 @@ object Main {
|
||||
private val IN_PROJECT_OPTION = "in-project"
|
||||
private val VERSION_OPTION = "version"
|
||||
private val JSON_OPTION = "json"
|
||||
private val LOG_LEVEL = "log-level"
|
||||
private val LOGGER_CONNECT = "logger-connect"
|
||||
|
||||
/**
|
||||
* Builds the [[Options]] object representing the CLI syntax.
|
||||
@ -151,6 +155,23 @@ object Main {
|
||||
.longOpt(JSON_OPTION)
|
||||
.desc("Switches the --version option to JSON output.")
|
||||
.build
|
||||
val logLevelOption = CliOption.builder
|
||||
.hasArg(true)
|
||||
.numberOfArgs(1)
|
||||
.argName("log-level")
|
||||
.longOpt(LOG_LEVEL)
|
||||
.desc(
|
||||
"Sets the runtime log level. Possible values are: OFF, ERROR, " +
|
||||
"WARNING, INFO, DEBUG and TRACE. Default: INFO."
|
||||
)
|
||||
.build
|
||||
val loggerConnectOption = CliOption.builder
|
||||
.hasArg(true)
|
||||
.numberOfArgs(1)
|
||||
.argName("uri")
|
||||
.longOpt(LOGGER_CONNECT)
|
||||
.desc("Connects to a logging service server and passes all logs to it.")
|
||||
.build
|
||||
|
||||
val options = new Options
|
||||
options
|
||||
@ -170,6 +191,8 @@ object Main {
|
||||
.addOption(inProjectOption)
|
||||
.addOption(version)
|
||||
.addOption(json)
|
||||
.addOption(logLevelOption)
|
||||
.addOption(loggerConnectOption)
|
||||
|
||||
options
|
||||
}
|
||||
@ -183,10 +206,16 @@ object Main {
|
||||
new HelpFormatter().printHelp(LanguageInfo.ID, options)
|
||||
|
||||
/** Terminates the process with a failure exit code. */
|
||||
private def exitFail(): Nothing = sys.exit(1)
|
||||
private def exitFail(): Nothing = exit(1)
|
||||
|
||||
/** Terminates the process with a success exit code. */
|
||||
private def exitSuccess(): Unit = sys.exit(0)
|
||||
private def exitSuccess(): Nothing = exit(0)
|
||||
|
||||
/** Shuts down the logging service and terminates the process. */
|
||||
private def exit(exitCode: Int): Nothing = {
|
||||
RunnerLogging.tearDown()
|
||||
sys.exit(exitCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the `--new` CLI option.
|
||||
@ -236,8 +265,13 @@ object Main {
|
||||
* @param path path of the project or file to execute
|
||||
* @param projectPath if specified, the script is run in context of a
|
||||
* project located at that path
|
||||
* @param logLevel log level to set for the engine runtime
|
||||
*/
|
||||
private def run(path: String, projectPath: Option[String]): Unit = {
|
||||
private def run(
|
||||
path: String,
|
||||
projectPath: Option[String],
|
||||
logLevel: LogLevel
|
||||
): Unit = {
|
||||
val file = new File(path)
|
||||
if (!file.exists) {
|
||||
println(s"File $file does not exist.")
|
||||
@ -263,7 +297,8 @@ object Main {
|
||||
System.in,
|
||||
System.out,
|
||||
Repl(TerminalIO()),
|
||||
strictErrors = true
|
||||
strictErrors = true,
|
||||
logLevel = logLevel
|
||||
)
|
||||
if (projectMode) {
|
||||
val pkg = PackageManager.Default.fromDirectory(file)
|
||||
@ -369,9 +404,10 @@ object Main {
|
||||
*
|
||||
* @param projectPath if specified, the REPL is run in context of a project
|
||||
* at the given path
|
||||
* @param logLevel log level to set for the engine runtime
|
||||
*/
|
||||
private def runRepl(projectPath: Option[String]): Unit = {
|
||||
val mainMethodName = "internal_repl_entry_point___"
|
||||
private def runRepl(projectPath: Option[String], logLevel: LogLevel): Unit = {
|
||||
val mainMethodName = "internal_repl_entry_point___"
|
||||
// TODO[MK, RW]: when CI-testing can use a fully-built distribution,
|
||||
// switch to `from Base import all` here.
|
||||
val dummySourceToTriggerRepl =
|
||||
@ -379,14 +415,15 @@ object Main {
|
||||
|
|
||||
|$mainMethodName = Debug.breakpoint
|
||||
|""".stripMargin
|
||||
val replModuleName = "Internal_Repl_Module___"
|
||||
val packagePath = projectPath.getOrElse("")
|
||||
val replModuleName = "Internal_Repl_Module___"
|
||||
val packagePath = projectPath.getOrElse("")
|
||||
val context =
|
||||
new ContextFactory().create(
|
||||
packagePath,
|
||||
System.in,
|
||||
System.out,
|
||||
Repl(TerminalIO())
|
||||
Repl(TerminalIO()),
|
||||
logLevel = logLevel
|
||||
)
|
||||
val mainModule =
|
||||
context.evalModule(dummySourceToTriggerRepl, replModuleName)
|
||||
@ -398,8 +435,11 @@ object Main {
|
||||
* Handles `--server` CLI option
|
||||
*
|
||||
* @param line a CLI line
|
||||
* @param logLevel log level to set for the engine runtime
|
||||
*/
|
||||
private def runLanguageServer(line: CommandLine): Unit = {
|
||||
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 {
|
||||
@ -452,6 +492,36 @@ object Main {
|
||||
println(versionDescription.asString(useJson))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the log level option.
|
||||
*/
|
||||
def parseLogLevel(levelOption: String): LogLevel = {
|
||||
val name = levelOption.toLowerCase
|
||||
LogLevel.allLevels.find(_.toString.toLowerCase == name).getOrElse {
|
||||
val possible =
|
||||
LogLevel.allLevels.map(_.toString.toLowerCase).mkString(", ")
|
||||
System.err.println(s"Invalid log level. Possible values are $possible.")
|
||||
exitFail()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an URI that specifies the logging service connection.
|
||||
*/
|
||||
def parseUri(string: String): Uri =
|
||||
try {
|
||||
Uri(string)
|
||||
} catch {
|
||||
case _: IllegalUriException =>
|
||||
System.err.println(s"`$string` is not a valid URI.")
|
||||
exitFail()
|
||||
}
|
||||
|
||||
/**
|
||||
* Default log level to use if the LOG_LEVEL option is not provided.
|
||||
*/
|
||||
val defaultLogLevel: LogLevel = LogLevel.Info
|
||||
|
||||
/**
|
||||
* Main entry point for the CLI program.
|
||||
*
|
||||
@ -472,6 +542,14 @@ object Main {
|
||||
displayVersion(useJson = line.hasOption(JSON_OPTION))
|
||||
exitSuccess()
|
||||
}
|
||||
|
||||
val logLevel = Option(line.getOptionValue(LOG_LEVEL))
|
||||
.map(parseLogLevel)
|
||||
.getOrElse(defaultLogLevel)
|
||||
val connectionUri =
|
||||
Option(line.getOptionValue(LOGGER_CONNECT)).map(parseUri)
|
||||
RunnerLogging.setup(connectionUri, logLevel)
|
||||
|
||||
if (line.hasOption(NEW_OPTION)) {
|
||||
createNew(
|
||||
path = line.getOptionValue(NEW_OPTION),
|
||||
@ -480,17 +558,19 @@ object Main {
|
||||
authorEmail = Option(line.getOptionValue(PROJECT_AUTHOR_EMAIL_OPTION))
|
||||
)
|
||||
}
|
||||
|
||||
if (line.hasOption(RUN_OPTION)) {
|
||||
run(
|
||||
line.getOptionValue(RUN_OPTION),
|
||||
Option(line.getOptionValue(IN_PROJECT_OPTION))
|
||||
Option(line.getOptionValue(IN_PROJECT_OPTION)),
|
||||
logLevel
|
||||
)
|
||||
}
|
||||
if (line.hasOption(REPL_OPTION)) {
|
||||
runRepl(Option(line.getOptionValue(IN_PROJECT_OPTION)))
|
||||
runRepl(Option(line.getOptionValue(IN_PROJECT_OPTION)), logLevel)
|
||||
}
|
||||
if (line.hasOption(LANGUAGE_SERVER_OPTION)) {
|
||||
runLanguageServer(line)
|
||||
runLanguageServer(line, logLevel)
|
||||
}
|
||||
printHelp(options)
|
||||
exitFail()
|
||||
|
@ -0,0 +1,72 @@
|
||||
package org.enso.runner
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.enso.loggingservice.printers.StderrPrinter
|
||||
import org.enso.loggingservice.{LogLevel, LoggerMode, LoggingServiceManager}
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* Manages setting up the logging service within the runner.
|
||||
*/
|
||||
object RunnerLogging {
|
||||
|
||||
/**
|
||||
* Sets up the runner's logging service.
|
||||
*
|
||||
* If `connectionUri` is provided it tries to connect to a logging service
|
||||
* server and pass logs to it. If it is not provided, or the connection could
|
||||
* not be established, falls back to logging to standard error output.
|
||||
*
|
||||
* @param connectionUri optional uri of logging service server to connect to
|
||||
* @param logLevel log level to use for the runner and runtime
|
||||
*/
|
||||
def setup(connectionUri: Option[Uri], logLevel: LogLevel): Unit = {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
val loggerSetup = connectionUri match {
|
||||
case Some(uri) =>
|
||||
LoggingServiceManager
|
||||
.setup(
|
||||
LoggerMode.Client(uri),
|
||||
logLevel
|
||||
)
|
||||
.map { _ =>
|
||||
logger.trace(s"Connected to logging service at `$uri`.")
|
||||
}
|
||||
.recoverWith { _ =>
|
||||
logger.error(
|
||||
"Failed to connect to the logging service server, " +
|
||||
"falling back to local logging."
|
||||
)
|
||||
setupLocalLogger(logLevel)
|
||||
}
|
||||
case None =>
|
||||
setupLocalLogger(logLevel)
|
||||
}
|
||||
loggerSetup.onComplete {
|
||||
case Failure(exception) =>
|
||||
System.err.println(s"Failed to initialize logging: $exception")
|
||||
exception.printStackTrace()
|
||||
case Success(_) =>
|
||||
}
|
||||
}
|
||||
|
||||
private def setupLocalLogger(logLevel: LogLevel): Future[Unit] =
|
||||
LoggingServiceManager
|
||||
.setup(
|
||||
LoggerMode.Local(
|
||||
Seq(StderrPrinter.create(printExceptions = true))
|
||||
),
|
||||
logLevel
|
||||
)
|
||||
|
||||
private val logger = Logger[RunnerLogging.type]
|
||||
|
||||
/**
|
||||
* Shuts down the logging service gracefully.
|
||||
*/
|
||||
def tearDown(): Unit =
|
||||
LoggingServiceManager.tearDown()
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
package org.enso.interpreter.runtime.number;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives;
|
||||
import com.oracle.truffle.api.interop.TruffleObject;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/** Internal wrapper for a {@link BigInteger}. */
|
||||
public class EnsoBigInteger {
|
||||
public class EnsoBigInteger implements TruffleObject {
|
||||
private final BigInteger value;
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,18 @@
|
||||
package org.enso.nativeimage.workarounds;
|
||||
|
||||
import com.oracle.svm.core.annotate.Substitute;
|
||||
import com.oracle.svm.core.annotate.TargetClass;
|
||||
|
||||
/**
|
||||
* Uses Native Image substitution capability to substitute the {@link
|
||||
* akka.dispatch.affinity.OnSpinWait.spinWait} function which causes problems when building the
|
||||
* Native Image.
|
||||
*/
|
||||
@TargetClass(className = "akka.dispatch.affinity.OnSpinWait")
|
||||
final class ReplacementAkkaSpinWait {
|
||||
|
||||
@Substitute
|
||||
public static void spinWait() {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.enso.nativeimage.workarounds;
|
||||
|
||||
import com.oracle.svm.core.annotate.Substitute;
|
||||
import com.oracle.svm.core.annotate.TargetClass;
|
||||
|
||||
/**
|
||||
* Uses Native Image substitution capability to substitute the {@link
|
||||
* scala.runtime.Statics#releaseFence()} function which causes problems when building the Native
|
||||
* Image on GraalVM 20.2.0.
|
||||
*/
|
||||
@TargetClass(className = "scala.runtime.Statics")
|
||||
final class ReplacementStatics {
|
||||
|
||||
/**
|
||||
* Implements a "release fence" without using an unsupported {@link java.lang.invoke.MethodHandle}
|
||||
* like the original one.
|
||||
* <p>
|
||||
* Instead, uses {@link sun.misc.Unsafe#storeFence()} under the hood.
|
||||
*/
|
||||
@Substitute
|
||||
public static void releaseFence() {
|
||||
Unsafe.unsafeInstance().storeFence();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
Args = -H:ReflectionConfigurationResources=${.}/reflect-config.json
|
@ -0,0 +1 @@
|
||||
[{ "name": "[B" }]
|
@ -1,4 +1,4 @@
|
||||
package org.enso.launcher.workarounds
|
||||
package org.enso.nativeimage.workarounds
|
||||
|
||||
/**
|
||||
* Gives access to an instance of [[sun.misc.Unsafe]] which contains low-level
|
@ -53,6 +53,14 @@ object CLIOutput {
|
||||
Predef.println(alignAndWrap(text))
|
||||
}
|
||||
|
||||
def readLine(): String = {
|
||||
Option(Console.in.readLine()).getOrElse(
|
||||
throw new RuntimeException(
|
||||
"End of stream has been reached when asking for user input."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints out the given question and asks the user to confirm the action by
|
||||
* typing 'y' or 'n'.
|
||||
@ -72,7 +80,7 @@ object CLIOutput {
|
||||
val prompt = if (yesDefault) "[Y/n]" else "[y/N]"
|
||||
val text = alignAndWrap(question + " " + prompt + " ")
|
||||
Predef.print(text)
|
||||
val line = Console.in.readLine().strip().toLowerCase
|
||||
val line = readLine().strip().toLowerCase
|
||||
if (line.isEmpty) yesDefault
|
||||
else if (line == "y") true
|
||||
else if (line == "n") false
|
||||
@ -126,7 +134,7 @@ object CLIOutput {
|
||||
question + " " + explanations + " " + shortcuts + " "
|
||||
)
|
||||
Predef.print(prompt)
|
||||
val line = Console.in.readLine().strip().toLowerCase
|
||||
val line = readLine().strip().toLowerCase
|
||||
if (line.isEmpty)
|
||||
answers.head
|
||||
else
|
||||
|
@ -41,10 +41,17 @@ trait Opts[+A] {
|
||||
* A callback for arguments.
|
||||
*
|
||||
* Should not be called if [[wantsArgument]] returns false.
|
||||
*
|
||||
* @param arg argument to consume
|
||||
* @param commandPrefix current command prefix to display in error/help
|
||||
* messages
|
||||
* @param suppressUnexpectedArgument if set, unexpected argument error should
|
||||
* be suppressed
|
||||
*/
|
||||
private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
commandPrefix: Seq[String],
|
||||
suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation
|
||||
|
||||
/**
|
||||
@ -304,8 +311,22 @@ object Opts {
|
||||
*/
|
||||
def hidden: Opts[A] = new HiddenOpts(opts)
|
||||
}
|
||||
|
||||
implicit class MapWithErrorsSyntax[A](val opts: Opts[A]) {
|
||||
|
||||
/**
|
||||
* Allows to map an Opts instance in a way that may result in an error.
|
||||
*
|
||||
* If `f` returns a [[Left]], a parse error is reported. Otherwise,
|
||||
* proceeds as `map` would with the result of `Right`.
|
||||
*/
|
||||
def mapWithErrors[B](f: A => Either[OptsParseError, B]): Opts[B] =
|
||||
new OptsMapWithErrors(opts, f)
|
||||
}
|
||||
}
|
||||
|
||||
import implicits._
|
||||
|
||||
/**
|
||||
* An option that accepts a single (required) positional argument and returns
|
||||
* its value.
|
||||
@ -425,6 +446,8 @@ object Opts {
|
||||
* @param metavar the name of the argument of the parameter, displayed in
|
||||
* help
|
||||
* @param help the help message included in the available options list
|
||||
* @param showInUsage specifies whether this flag should be included in the
|
||||
* usage command line
|
||||
* @tparam A type of the value that is parsed; needs an [[Argument]]
|
||||
* instance
|
||||
*/
|
||||
@ -436,6 +459,61 @@ object Opts {
|
||||
): Opts[Option[A]] =
|
||||
new OptionalParameter[A](name, metavar, help, showInUsage)
|
||||
|
||||
/**
|
||||
* An optional parameter with multiple aliases.
|
||||
*
|
||||
* Returns a value if it is present for exactly one of the aliases or none if
|
||||
* no alias is present. If values are present for mulitple aliases, raises a
|
||||
* parse error.
|
||||
*
|
||||
* @param primaryName primary name that is displayed in help and suggestions
|
||||
* @param aliases additional aliases
|
||||
* @param metavar the name of the argument of the parameter, displayed in
|
||||
* help
|
||||
* @param help the help message included in the available options list
|
||||
* @param showInUsage specifies whether this flag should be included in the
|
||||
* usage command line
|
||||
* @tparam A type of the value that is parsed; needs an [[Argument]]
|
||||
* instance
|
||||
*/
|
||||
def aliasedOptionalParameter[A: Argument](
|
||||
primaryName: String,
|
||||
aliases: String*
|
||||
)(
|
||||
metavar: String,
|
||||
help: String,
|
||||
showInUsage: Boolean = false
|
||||
): Opts[Option[A]] = {
|
||||
def withName(name: String)(option: Option[A]): Option[(A, String)] =
|
||||
option.map((_, name))
|
||||
val primaryOpt = optionalParameter[A](
|
||||
primaryName,
|
||||
metavar,
|
||||
help,
|
||||
showInUsage = showInUsage
|
||||
).map(withName(primaryName))
|
||||
val aliasedOpts = aliases.map(aliasName =>
|
||||
optionalParameter[A](aliasName, metavar, help, showInUsage = false)
|
||||
.map(withName(aliasName))
|
||||
.hidden
|
||||
)
|
||||
sequence(primaryOpt :: aliasedOpts.toList).mapWithErrors { resultOptions =>
|
||||
val results = resultOptions.flatten
|
||||
results match {
|
||||
case Nil => Right(None)
|
||||
case (one, _) :: Nil => Right(Some(one))
|
||||
case more =>
|
||||
val presentOptions = more.map(res => s"`--${res._2}`")
|
||||
OptsParseError.left(
|
||||
s"Multiple values for aliases of the same option " +
|
||||
s"(${presentOptions.init.mkString(", ")} and " +
|
||||
s"${presentOptions.last}) have been provided. Please provide " +
|
||||
s"just one value for `--$primaryName`."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An option that accepts an arbitrary amount of parameters with a fixed
|
||||
* prefix and returns a sequence of tuples that contain a suffix and a
|
||||
@ -502,4 +580,25 @@ object Opts {
|
||||
* value.
|
||||
*/
|
||||
def pure[A](a: A): Opts[A] = new OptsPure[A](a)
|
||||
|
||||
/**
|
||||
* Turns a sequence of options into a single option that returns results of
|
||||
* these options, if all of them parsed successfully.
|
||||
*/
|
||||
def sequence[A](opts: Seq[Opts[A]]): Opts[Seq[A]] =
|
||||
sequence(opts.toList).map(_.toSeq)
|
||||
|
||||
/**
|
||||
* Turns a list of options into a single option that returns results of
|
||||
* these options, if all of them parsed successfully.
|
||||
*/
|
||||
def sequence[A](opts: List[Opts[A]]): Opts[List[A]] =
|
||||
opts match {
|
||||
case Nil => Opts.pure(Nil)
|
||||
case head :: tail =>
|
||||
val tailSequenced = sequence(tail)
|
||||
(head, tailSequenced) mapN { (headResult, tailResult) =>
|
||||
headResult :: tailResult
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +89,11 @@ object Parser {
|
||||
tokenProvider.consumeToken() match {
|
||||
case PlainToken(value) =>
|
||||
if (opts.wantsArgument()) {
|
||||
val continuation = opts.consumeArgument(value, Seq(applicationName))
|
||||
val continuation = opts.consumeArgument(
|
||||
value,
|
||||
Seq(applicationName),
|
||||
suppressUnexpectedArgument
|
||||
)
|
||||
continuation match {
|
||||
case ParserContinuation.ContinueNormally =>
|
||||
case ParserContinuation.Stop =>
|
||||
|
@ -3,6 +3,8 @@ package org.enso.cli.internal.opts
|
||||
import org.enso.cli.arguments.Opts
|
||||
import org.enso.cli.internal.ParserContinuation
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
abstract class BaseOpts[A] extends Opts[A] {
|
||||
override private[cli] val flags: Map[String, () => Unit] = Map.empty
|
||||
override private[cli] val parameters: Map[String, String => Unit] = Map.empty
|
||||
@ -15,8 +17,9 @@ abstract class BaseOpts[A] extends Opts[A] {
|
||||
|
||||
override private[cli] def wantsArgument() = false
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
@unused arg: String,
|
||||
@unused commandPrefix: Seq[String],
|
||||
@unused suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation =
|
||||
throw new IllegalStateException(
|
||||
"Internal error: " +
|
||||
|
@ -67,12 +67,13 @@ trait BaseSubcommandOpt[A, B] extends Opts[A] {
|
||||
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
commandPrefix: Seq[String],
|
||||
suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation = {
|
||||
val prefix = extendPrefix(commandPrefix)
|
||||
selectedCommand match {
|
||||
case Some(command) =>
|
||||
command.opts.consumeArgument(arg, prefix)
|
||||
command.opts.consumeArgument(arg, prefix, suppressUnexpectedArgument)
|
||||
case None =>
|
||||
availableSubcommands.find(_.name == arg) match {
|
||||
case Some(command) =>
|
||||
@ -88,7 +89,8 @@ trait BaseSubcommandOpt[A, B] extends Opts[A] {
|
||||
addError(relatedMessage)
|
||||
ParserContinuation.Stop
|
||||
case None =>
|
||||
handleUnknownCommand(arg)
|
||||
if (suppressUnexpectedArgument) ParserContinuation.Stop
|
||||
else handleUnknownCommand(arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ class HiddenOpts[A](opts: Opts[A]) extends Opts[A] {
|
||||
override private[cli] def wantsArgument() = opts.wantsArgument()
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
commandPrefix: Seq[String],
|
||||
suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation =
|
||||
opts.consumeArgument(arg, commandPrefix)
|
||||
opts.consumeArgument(arg, commandPrefix, suppressUnexpectedArgument)
|
||||
|
||||
override private[cli] val requiredArguments: Seq[String] = Seq()
|
||||
override private[cli] val optionalArguments: Seq[String] = Seq()
|
||||
|
@ -3,6 +3,8 @@ package org.enso.cli.internal.opts
|
||||
import org.enso.cli.arguments.{Argument, OptsParseError}
|
||||
import org.enso.cli.internal.ParserContinuation
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
class OptionalPositionalArgument[A: Argument](
|
||||
metavar: String,
|
||||
helpComment: Option[String]
|
||||
@ -20,7 +22,8 @@ class OptionalPositionalArgument[A: Argument](
|
||||
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
@unused commandPrefix: Seq[String],
|
||||
@unused suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation = {
|
||||
value = for {
|
||||
parsed <- Argument[A].read(arg)
|
||||
|
@ -17,9 +17,10 @@ class OptsMap[A, B](a: Opts[A], f: A => B) extends Opts[B] {
|
||||
override private[cli] def wantsArgument() = a.wantsArgument()
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
commandPrefix: Seq[String],
|
||||
suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation =
|
||||
a.consumeArgument(arg, commandPrefix)
|
||||
a.consumeArgument(arg, commandPrefix, suppressUnexpectedArgument)
|
||||
override private[cli] def requiredArguments: Seq[String] = a.requiredArguments
|
||||
override private[cli] def optionalArguments: Seq[String] = a.optionalArguments
|
||||
override private[cli] def trailingArguments: Option[String] =
|
||||
|
@ -0,0 +1,46 @@
|
||||
package org.enso.cli.internal.opts
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import org.enso.cli.arguments.{Opts, OptsParseError}
|
||||
import org.enso.cli.internal.ParserContinuation
|
||||
|
||||
class OptsMapWithErrors[A, B](a: Opts[A], f: A => Either[OptsParseError, B])
|
||||
extends Opts[B] {
|
||||
override private[cli] def flags = a.flags
|
||||
override private[cli] def parameters = a.parameters
|
||||
override private[cli] def prefixedParameters = a.prefixedParameters
|
||||
override private[cli] def usageOptions = a.usageOptions
|
||||
override private[cli] def gatherOptions =
|
||||
a.gatherOptions
|
||||
override private[cli] def gatherPrefixedParameters =
|
||||
a.gatherPrefixedParameters
|
||||
|
||||
override private[cli] def wantsArgument() = a.wantsArgument()
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String],
|
||||
suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation =
|
||||
a.consumeArgument(arg, commandPrefix, suppressUnexpectedArgument)
|
||||
override private[cli] def requiredArguments: Seq[String] = a.requiredArguments
|
||||
override private[cli] def optionalArguments: Seq[String] = a.optionalArguments
|
||||
override private[cli] def trailingArguments: Option[String] =
|
||||
a.trailingArguments
|
||||
|
||||
override private[cli] def additionalArguments = a.additionalArguments
|
||||
|
||||
override private[cli] def reset(): Unit = a.reset()
|
||||
|
||||
override private[cli] def result(
|
||||
commandPrefix: Seq[String]
|
||||
): Either[OptsParseError, B] = a.result(commandPrefix).flatMap(f)
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] = a.availableOptionsHelp()
|
||||
override def availablePrefixedParametersHelp(): Seq[String] =
|
||||
a.availablePrefixedParametersHelp()
|
||||
override def additionalHelp(): Seq[String] = a.additionalHelp()
|
||||
|
||||
override def commandLines(
|
||||
alwaysIncludeOtherOptions: Boolean = false
|
||||
): NonEmptyList[String] = a.commandLines(alwaysIncludeOtherOptions)
|
||||
}
|
@ -20,10 +20,16 @@ class OptsProduct[A, B](lhs: Opts[A], rhs: Opts[B]) extends Opts[(A, B)] {
|
||||
lhs.wantsArgument() || rhs.wantsArgument()
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
commandPrefix: Seq[String],
|
||||
suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation =
|
||||
if (lhs.wantsArgument()) lhs.consumeArgument(arg, commandPrefix)
|
||||
else rhs.consumeArgument(arg, commandPrefix)
|
||||
if (lhs.wantsArgument())
|
||||
lhs.consumeArgument(
|
||||
arg,
|
||||
commandPrefix,
|
||||
suppressUnexpectedArgument
|
||||
)
|
||||
else rhs.consumeArgument(arg, commandPrefix, suppressUnexpectedArgument)
|
||||
override private[cli] def requiredArguments: Seq[String] =
|
||||
lhs.requiredArguments ++ rhs.requiredArguments
|
||||
override private[cli] def optionalArguments: Seq[String] =
|
||||
|
@ -3,6 +3,8 @@ package org.enso.cli.internal.opts
|
||||
import org.enso.cli.arguments.{Argument, OptsParseError}
|
||||
import org.enso.cli.internal.ParserContinuation
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
class PositionalArgument[A: Argument](
|
||||
metavar: String,
|
||||
helpComment: Option[String]
|
||||
@ -20,7 +22,8 @@ class PositionalArgument[A: Argument](
|
||||
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
@unused commandPrefix: Seq[String],
|
||||
@unused suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation = {
|
||||
value = for {
|
||||
parsed <- Argument[A].read(arg)
|
||||
|
@ -3,6 +3,8 @@ package org.enso.cli.internal.opts
|
||||
import org.enso.cli.arguments.{Argument, OptsParseError}
|
||||
import org.enso.cli.internal.ParserContinuation
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
class TrailingArguments[A: Argument](
|
||||
metavar: String,
|
||||
helpComment: Option[String]
|
||||
@ -16,7 +18,8 @@ class TrailingArguments[A: Argument](
|
||||
|
||||
override private[cli] def consumeArgument(
|
||||
arg: String,
|
||||
commandPrefix: Seq[String]
|
||||
@unused commandPrefix: Seq[String],
|
||||
@unused suppressUnexpectedArgument: Boolean
|
||||
): ParserContinuation = {
|
||||
value = for {
|
||||
currentArguments <- value
|
||||
|
@ -0,0 +1,19 @@
|
||||
package org.enso.loggingservice.internal;
|
||||
|
||||
/**
|
||||
* Provides a stub for enabling VT console mode.
|
||||
* <p>
|
||||
* We assume that VT is supported by default on UNIX platforms, so this function is never actually
|
||||
* used. It is defined just to provide binary compatibility with the Windows counterpart, so that
|
||||
* the code that uses it only on Windows, compiles on all platforms.
|
||||
*/
|
||||
public class NativeAnsiTerm {
|
||||
|
||||
/**
|
||||
* Enables VT emulation within the connected console.
|
||||
* <p>
|
||||
* The UNIX variant does nothing, as we assume that VT is supported out of the box.
|
||||
*/
|
||||
public static void enableVT() {
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package org.enso.loggingservice.internal;
|
||||
|
||||
import org.graalvm.nativeimage.UnmanagedMemory;
|
||||
import org.graalvm.nativeimage.c.function.CFunction;
|
||||
import org.graalvm.nativeimage.c.type.CIntPointer;
|
||||
import org.graalvm.word.PointerBase;
|
||||
|
||||
/**
|
||||
* Provides access to the native WinApi calls that enable VT emulation in a connected console.
|
||||
*/
|
||||
public class NativeAnsiTerm {
|
||||
|
||||
/**
|
||||
* Returns a handle to a console connected to one of the standard streams.
|
||||
* <p>
|
||||
* Refer to: https://docs.microsoft.com/en-us/windows/console/getstdhandle
|
||||
*
|
||||
* @param nStdHandle constant representing one of the standard streams
|
||||
* @return pointer to the console handle or null if it could not be accessed
|
||||
*/
|
||||
@CFunction
|
||||
private static native PointerBase GetStdHandle(int nStdHandle);
|
||||
|
||||
/**
|
||||
* Returns current console mode.
|
||||
*
|
||||
* @param hConsoleHandle console handle from [[GetStdHandle]]
|
||||
* @param lpMode pointer to an integer that will be set to the current mode on success
|
||||
* @return non-zero integer on success
|
||||
* @see <a href="https://docs.microsoft.com/en-us/windows/console/setconsolemodehttps://docs.microsoft.com/en-us/windows/console/getconsolemode>GetConsoleMode</a>
|
||||
*/
|
||||
@CFunction
|
||||
private static native int GetConsoleMode(PointerBase hConsoleHandle, CIntPointer lpMode);
|
||||
|
||||
/**
|
||||
* Sets console mode.
|
||||
*
|
||||
* @param hConsoleHandle console handle from [[GetStdHandle]]
|
||||
* @param dwMode mode to set
|
||||
* @return non-zero integer on success
|
||||
* @see <a href="https://docs.microsoft.com/en-us/windows/console/setconsolemode">SetConsoleMode</a>
|
||||
*/
|
||||
@CFunction
|
||||
private static native int SetConsoleMode(PointerBase hConsoleHandle, int dwMode);
|
||||
|
||||
/**
|
||||
* Returns error code of last error.
|
||||
* <p>
|
||||
* Can be called if a function returns a zero exit code to get the error code.
|
||||
*
|
||||
* @see <a href="https://docs.microsoft.com/en-gb/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror">GetLastError</a>
|
||||
*/
|
||||
@CFunction
|
||||
private static native int GetLastError();
|
||||
|
||||
/**
|
||||
* Constant that can be used in [[GetStdHandle]] that refers to the standard error stream.
|
||||
*
|
||||
* @see <a href="https://docs.microsoft.com/en-us/windows/console/getstdhandle">GetStdHandle</a>
|
||||
*/
|
||||
private final static int STD_ERROR_HANDLE = -12;
|
||||
|
||||
/**
|
||||
* Constant that can be used as part of a console mode which indicates that the output stream
|
||||
* should handle VT escape codes.
|
||||
*
|
||||
* @see <a href="https://docs.microsoft.com/en-us/windows/console/setconsolemode">SetConsoleMode</a>
|
||||
* @see <a href="https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences">Console
|
||||
* Virtual Terminal Sequences</a>
|
||||
*/
|
||||
private final static int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
|
||||
|
||||
/**
|
||||
* Enables VT emulation within the connected console.
|
||||
* <p>
|
||||
* May throw an exception if it is not possible to do so. Can only be called from native-image
|
||||
* targets.
|
||||
*/
|
||||
public static void enableVT() {
|
||||
CIntPointer modePtr = UnmanagedMemory.malloc(4);
|
||||
try {
|
||||
var handle = GetStdHandle(STD_ERROR_HANDLE);
|
||||
if (handle.isNull()) {
|
||||
throw new RuntimeException("Failed to get console handle. " +
|
||||
"Perhaps the console is not connected. " +
|
||||
"Error code: " + GetLastError());
|
||||
}
|
||||
if (GetConsoleMode(handle, modePtr) == 0) {
|
||||
throw new RuntimeException("Failed to get console mode. " +
|
||||
"Error code: " + GetLastError());
|
||||
}
|
||||
var alteredMode =
|
||||
modePtr.read() | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
if (SetConsoleMode(handle, alteredMode) == 0) {
|
||||
throw new RuntimeException("Failed to set console mode. " +
|
||||
"Perhaps the console does not support VT codes. " +
|
||||
"Error code: " + GetLastError());
|
||||
}
|
||||
} finally {
|
||||
UnmanagedMemory.free(modePtr);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package org.slf4j.impl;
|
||||
|
||||
import org.enso.loggingservice.LoggerFactory;
|
||||
import org.slf4j.ILoggerFactory;
|
||||
|
||||
/**
|
||||
* Binds the logging service as an SLF4J backend.
|
||||
* <p>
|
||||
* The public interface of this class must conform to what is expected by an
|
||||
* SLF4J backend. See slf4j-simple for reference.
|
||||
*/
|
||||
public class StaticLoggerBinder {
|
||||
/**
|
||||
* Should be in sync with `slf4jVersion` in `build.sbt`.
|
||||
*/
|
||||
public static String REQUESTED_API_VERSION = "1.7.30";
|
||||
|
||||
final private static StaticLoggerBinder singleton = new StaticLoggerBinder();
|
||||
|
||||
public static StaticLoggerBinder getSingleton() {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
private final LoggerFactory factory = new LoggerFactory();
|
||||
private final String factoryClassStr = LoggerFactory.class.getName();
|
||||
|
||||
public ILoggerFactory getLoggerFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
public String getLoggerFactoryClassStr() {
|
||||
return factoryClassStr;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.slf4j.impl;
|
||||
|
||||
import org.slf4j.helpers.NOPMDCAdapter;
|
||||
import org.slf4j.spi.MDCAdapter;
|
||||
|
||||
/**
|
||||
* Provides a no-op MDC adapter for the SLF4J backend.
|
||||
* <p>
|
||||
* MDC handling is an optional SLF4J feature and currently the logging service does not support it,
|
||||
* so it provides a no-op adapter.
|
||||
* <p>
|
||||
* The public interface of this class must conform to what is expected by an SLF4J backend. See
|
||||
* slf4j-simple for reference.
|
||||
*/
|
||||
public class StaticMDCBinder {
|
||||
|
||||
final private static StaticMDCBinder singleton = new StaticMDCBinder();
|
||||
|
||||
public static StaticMDCBinder getSingleton() {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
private final MDCAdapter adapter = new NOPMDCAdapter();
|
||||
private final String adapterClassStr = NOPMDCAdapter.class.getName();
|
||||
|
||||
public MDCAdapter getMDCA() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
public String getMDCAdapterClassStr() {
|
||||
return adapterClassStr;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.slf4j.impl;
|
||||
|
||||
import org.slf4j.IMarkerFactory;
|
||||
import org.slf4j.helpers.BasicMarkerFactory;
|
||||
|
||||
/**
|
||||
* Provides a simple marker factory for the SLF4J backend.
|
||||
* <p>
|
||||
* The public interface of this class must conform to what is expected by an SLF4J backend. See
|
||||
* slf4j-simple for reference.
|
||||
*/
|
||||
public class StaticMarkerBinder {
|
||||
|
||||
final private static StaticMarkerBinder singleton = new StaticMarkerBinder();
|
||||
|
||||
public static StaticMarkerBinder getSingleton() {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
private final IMarkerFactory markerFactory = new BasicMarkerFactory();
|
||||
private final String markerFactoryClassStr =
|
||||
BasicMarkerFactory.class.getName();
|
||||
|
||||
public IMarkerFactory getMarkerFactory() {
|
||||
return markerFactory;
|
||||
}
|
||||
|
||||
public String getMarkerFactoryClassStr() {
|
||||
return markerFactoryClassStr;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package org.enso
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
|
||||
package object logger {
|
||||
|
||||
/**
|
||||
* Provides syntax for entering a sub-logger.
|
||||
*/
|
||||
implicit class LoggerSyntax(logger: Logger) {
|
||||
|
||||
/**
|
||||
* Returns another [[Logger]] with name extended with a sub context.
|
||||
*/
|
||||
def enter(subContextName: String): Logger = {
|
||||
val name = logger.underlying.getName + "." + subContextName
|
||||
Logger(name)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import java.util.logging.{Handler, Level, LogRecord}
|
||||
|
||||
import org.enso.loggingservice.internal.{InternalLogMessage, LoggerConnection}
|
||||
|
||||
/**
|
||||
* A [[Handler]] implementation that allows to use the logging service as a
|
||||
* backend for [[java.util.logging]].
|
||||
*/
|
||||
class JavaLoggingLogHandler(
|
||||
levelMapping: Level => LogLevel,
|
||||
connection: LoggerConnection
|
||||
) extends Handler {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def publish(record: LogRecord): Unit = {
|
||||
val level = levelMapping(record.getLevel)
|
||||
if (connection.isEnabled(level)) {
|
||||
val message = InternalLogMessage(
|
||||
level = level,
|
||||
timestamp = record.getInstant,
|
||||
group = record.getLoggerName,
|
||||
message = record.getMessage,
|
||||
exception = Option(record.getThrown)
|
||||
)
|
||||
connection.send(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def flush(): Unit = {}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def close(): Unit = {}
|
||||
}
|
||||
|
||||
object JavaLoggingLogHandler {
|
||||
|
||||
/**
|
||||
* Creates a [[Handler]] with the provided mapping from Java's log levels to
|
||||
* our log levels.
|
||||
*/
|
||||
def create(mapping: Level => LogLevel): JavaLoggingLogHandler =
|
||||
new JavaLoggingLogHandler(mapping, LoggingServiceManager.Connection)
|
||||
|
||||
/**
|
||||
* Determines what is the smallest Java level that is still debug and not
|
||||
* trace.
|
||||
*/
|
||||
private val defaultLevelDebugCutOff =
|
||||
Seq(Level.FINE.intValue, Level.CONFIG.intValue).min
|
||||
|
||||
/**
|
||||
* Default mapping of Java log levels to our log levels based
|
||||
*/
|
||||
def defaultLevelMapping(javaLevel: Level): LogLevel = {
|
||||
val level = javaLevel.intValue
|
||||
if (level == Level.OFF.intValue) LogLevel.Off
|
||||
else if (level >= Level.SEVERE.intValue) LogLevel.Error
|
||||
else if (level >= Level.WARNING.intValue) LogLevel.Warning
|
||||
else if (level >= Level.INFO.intValue) LogLevel.Info
|
||||
else if (level >= defaultLevelDebugCutOff) LogLevel.Debug
|
||||
else LogLevel.Trace
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate-inverse of [[defaultLevelMapping]] that returns a Java log
|
||||
* level corresponding to the given log level.
|
||||
*/
|
||||
def getJavaLogLevelFor(logLevel: LogLevel): Level =
|
||||
logLevel match {
|
||||
case LogLevel.Off => Level.OFF
|
||||
case LogLevel.Error => Level.SEVERE
|
||||
case LogLevel.Warning => Level.WARNING
|
||||
case LogLevel.Info => Level.INFO
|
||||
case LogLevel.Debug => Level.FINE
|
||||
case LogLevel.Trace => Level.ALL
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import io.circe.syntax._
|
||||
import io.circe.{Decoder, DecodingFailure, Encoder}
|
||||
|
||||
/**
|
||||
* Defines a log level for log messages.
|
||||
*/
|
||||
sealed abstract class LogLevel(final val level: Int) {
|
||||
|
||||
/**
|
||||
* Determines if a component running on `this` log level should log the
|
||||
* `other`.
|
||||
*
|
||||
* Log levels smaller or equal to component's log level are logged.
|
||||
*/
|
||||
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
|
||||
* component's log level to completely disable logging for it.
|
||||
*/
|
||||
case object Off extends LogLevel(-1) {
|
||||
override def toString: String = "off"
|
||||
}
|
||||
|
||||
/**
|
||||
* Log level corresponding to severe errors, should be understandable to the
|
||||
* end-user.
|
||||
*/
|
||||
case object Error extends LogLevel(0) {
|
||||
override def toString: String = "error"
|
||||
}
|
||||
|
||||
/**
|
||||
* Log level corresponding to important notices or issues that are not
|
||||
* severe.
|
||||
*/
|
||||
case object Warning extends LogLevel(1) {
|
||||
override def toString: String = "warning"
|
||||
}
|
||||
|
||||
/**
|
||||
* Log level corresponding to usual information of what the application is
|
||||
* doing.
|
||||
*/
|
||||
case object Info extends LogLevel(2) {
|
||||
override def toString: String = "info"
|
||||
}
|
||||
|
||||
/**
|
||||
* Log level used for debugging the application.
|
||||
*
|
||||
* The messages can be more complex and targeted at developers diagnosing the
|
||||
* application.
|
||||
*/
|
||||
case object Debug extends LogLevel(3) {
|
||||
override def toString: String = "debug"
|
||||
}
|
||||
|
||||
/**
|
||||
* Log level used for advanced debugging, may be used for more throughout
|
||||
* diagnostics.
|
||||
*/
|
||||
case object Trace extends LogLevel(4) {
|
||||
override def toString: String = "trace"
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all available log levels.
|
||||
*
|
||||
* Can be used for example to automate parsing.
|
||||
*/
|
||||
val allLevels = Seq(
|
||||
LogLevel.Off,
|
||||
LogLevel.Error,
|
||||
LogLevel.Warning,
|
||||
LogLevel.Info,
|
||||
LogLevel.Debug,
|
||||
LogLevel.Trace
|
||||
)
|
||||
|
||||
/**
|
||||
* [[Ordering]] instance for [[LogLevel]].
|
||||
*
|
||||
* The log levels are ordered from most severe. If a log level is enabled, it
|
||||
* usually means that all levels smaller than it are enabled too.
|
||||
*/
|
||||
implicit val ord: Ordering[LogLevel] = (x, y) => x.level - y.level
|
||||
|
||||
/**
|
||||
* [[Encoder]] instance for [[LogLevel]].
|
||||
*/
|
||||
implicit val encoder: Encoder[LogLevel] = {
|
||||
case Off =>
|
||||
throw new IllegalArgumentException(
|
||||
"`None` log level should never be used in actual log messages and it " +
|
||||
"cannot be serialized to prevent that."
|
||||
)
|
||||
case level =>
|
||||
level.level.asJson
|
||||
}
|
||||
|
||||
/**
|
||||
* [[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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,304 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import org.enso.loggingservice.internal.{InternalLogMessage, LoggerConnection}
|
||||
import org.slf4j.helpers.MessageFormatter
|
||||
import org.slf4j.{Logger => SLF4JLogger, Marker}
|
||||
|
||||
import scala.annotation.unused
|
||||
|
||||
/**
|
||||
* A [[SLF4JLogger]] instance for the SLF4J backend which passes all log
|
||||
* messages to a [[LoggerConnection]].
|
||||
*
|
||||
* @param name name of the logger
|
||||
* @param connection the connection to pass the log messages to
|
||||
*/
|
||||
class Logger(name: String, connection: LoggerConnection) extends SLF4JLogger {
|
||||
override def getName: String = name
|
||||
|
||||
private def isEnabled(level: LogLevel): Boolean =
|
||||
connection.isEnabled(level)
|
||||
|
||||
private def log(
|
||||
level: LogLevel,
|
||||
msg: String
|
||||
): Unit = {
|
||||
if (isEnabled(level)) {
|
||||
connection.send(InternalLogMessage(level, name, msg, None))
|
||||
}
|
||||
}
|
||||
|
||||
private def log(
|
||||
level: LogLevel,
|
||||
format: String,
|
||||
arg: AnyRef
|
||||
): Unit = {
|
||||
if (isEnabled(level)) {
|
||||
val fp = MessageFormatter.format(format, arg)
|
||||
connection.send(
|
||||
InternalLogMessage(level, name, fp.getMessage, Option(fp.getThrowable))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def log(
|
||||
level: LogLevel,
|
||||
format: String,
|
||||
arg1: AnyRef,
|
||||
arg2: AnyRef
|
||||
): Unit = {
|
||||
if (isEnabled(level)) {
|
||||
val fp = MessageFormatter.format(format, arg1, arg2)
|
||||
connection.send(
|
||||
InternalLogMessage(level, name, fp.getMessage, Option(fp.getThrowable))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def log(
|
||||
level: LogLevel,
|
||||
format: String,
|
||||
args: Seq[AnyRef]
|
||||
): Unit = {
|
||||
if (isEnabled(level)) {
|
||||
val fp = MessageFormatter.arrayFormat(format, args.toArray)
|
||||
connection.send(
|
||||
InternalLogMessage(level, name, fp.getMessage, Option(fp.getThrowable))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def log(
|
||||
level: LogLevel,
|
||||
msg: String,
|
||||
throwable: Throwable
|
||||
): Unit = {
|
||||
if (isEnabled(level)) {
|
||||
connection.send(
|
||||
InternalLogMessage(level, name, msg, Some(throwable))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def isTraceEnabled: Boolean = isEnabled(LogLevel.Trace)
|
||||
|
||||
override def trace(msg: String): Unit = log(LogLevel.Trace, msg)
|
||||
|
||||
override def trace(format: String, arg: AnyRef): Unit =
|
||||
log(LogLevel.Trace, format, arg)
|
||||
|
||||
override def trace(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
|
||||
log(LogLevel.Trace, format, arg1, arg2)
|
||||
|
||||
override def trace(format: String, arguments: AnyRef*): Unit =
|
||||
log(LogLevel.Trace, format, arguments)
|
||||
|
||||
override def trace(msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Trace, msg, t)
|
||||
|
||||
override def isTraceEnabled(@unused marker: Marker): Boolean =
|
||||
isEnabled(LogLevel.Trace)
|
||||
|
||||
override def trace(@unused marker: Marker, msg: String): Unit =
|
||||
log(LogLevel.Trace, msg)
|
||||
|
||||
override def trace(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg: AnyRef
|
||||
): Unit =
|
||||
log(LogLevel.Trace, format, arg)
|
||||
|
||||
override def trace(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg1: AnyRef,
|
||||
arg2: AnyRef
|
||||
): Unit = log(LogLevel.Trace, format, arg1, arg2)
|
||||
|
||||
override def trace(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
argArray: AnyRef*
|
||||
): Unit =
|
||||
log(LogLevel.Trace, format, argArray)
|
||||
|
||||
override def trace(@unused marker: Marker, msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Trace, msg, t)
|
||||
|
||||
override def isDebugEnabled: Boolean = isEnabled(LogLevel.Debug)
|
||||
|
||||
override def debug(msg: String): Unit = log(LogLevel.Debug, msg)
|
||||
|
||||
override def debug(format: String, arg: AnyRef): Unit =
|
||||
log(LogLevel.Debug, format, arg)
|
||||
|
||||
override def debug(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
|
||||
log(LogLevel.Debug, format, arg1, arg2)
|
||||
|
||||
override def debug(format: String, arguments: AnyRef*): Unit =
|
||||
log(LogLevel.Debug, format, arguments)
|
||||
|
||||
override def debug(msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Debug, msg, t)
|
||||
|
||||
override def isDebugEnabled(@unused marker: Marker): Boolean =
|
||||
isEnabled(LogLevel.Debug)
|
||||
|
||||
override def debug(@unused marker: Marker, msg: String): Unit =
|
||||
log(LogLevel.Debug, msg)
|
||||
|
||||
override def debug(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg: AnyRef
|
||||
): Unit =
|
||||
log(LogLevel.Debug, format, arg)
|
||||
|
||||
override def debug(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg1: AnyRef,
|
||||
arg2: AnyRef
|
||||
): Unit = log(LogLevel.Debug, format, arg1, arg2)
|
||||
|
||||
override def debug(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arguments: AnyRef*
|
||||
): Unit =
|
||||
log(LogLevel.Debug, format, arguments)
|
||||
|
||||
override def debug(@unused marker: Marker, msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Debug, msg, t)
|
||||
|
||||
override def isInfoEnabled: Boolean = isEnabled(LogLevel.Info)
|
||||
|
||||
override def info(msg: String): Unit = log(LogLevel.Info, msg)
|
||||
|
||||
override def info(format: String, arg: AnyRef): Unit =
|
||||
log(LogLevel.Info, format, arg)
|
||||
|
||||
override def info(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
|
||||
log(LogLevel.Info, format, arg1, arg2)
|
||||
|
||||
override def info(format: String, arguments: AnyRef*): Unit =
|
||||
log(LogLevel.Info, format, arguments)
|
||||
|
||||
override def info(msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Info, msg, t)
|
||||
|
||||
override def isInfoEnabled(@unused marker: Marker): Boolean =
|
||||
isEnabled(LogLevel.Info)
|
||||
|
||||
override def info(@unused marker: Marker, msg: String): Unit =
|
||||
log(LogLevel.Info, msg)
|
||||
|
||||
override def info(@unused marker: Marker, format: String, arg: AnyRef): Unit =
|
||||
log(LogLevel.Info, format, arg)
|
||||
|
||||
override def info(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg1: AnyRef,
|
||||
arg2: AnyRef
|
||||
): Unit = log(LogLevel.Info, format, arg1, arg2)
|
||||
|
||||
override def info(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arguments: AnyRef*
|
||||
): Unit =
|
||||
log(LogLevel.Info, format, arguments)
|
||||
|
||||
override def info(@unused marker: Marker, msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Info, msg, t)
|
||||
|
||||
override def isWarnEnabled: Boolean = isEnabled(LogLevel.Warning)
|
||||
|
||||
override def warn(msg: String): Unit = log(LogLevel.Warning, msg)
|
||||
|
||||
override def warn(format: String, arg: AnyRef): Unit =
|
||||
log(LogLevel.Warning, format, arg)
|
||||
|
||||
override def warn(format: String, arguments: AnyRef*): Unit =
|
||||
log(LogLevel.Warning, format, arguments)
|
||||
|
||||
override def warn(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
|
||||
log(LogLevel.Warning, format, arg1, arg2)
|
||||
|
||||
override def warn(msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Warning, msg, t)
|
||||
|
||||
override def isWarnEnabled(@unused marker: Marker): Boolean =
|
||||
isEnabled(LogLevel.Warning)
|
||||
|
||||
override def warn(@unused marker: Marker, msg: String): Unit =
|
||||
log(LogLevel.Warning, msg)
|
||||
|
||||
override def warn(@unused marker: Marker, format: String, arg: AnyRef): Unit =
|
||||
log(LogLevel.Warning, format, arg)
|
||||
|
||||
override def warn(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg1: AnyRef,
|
||||
arg2: AnyRef
|
||||
): Unit = log(LogLevel.Warning, format, arg1, arg2)
|
||||
|
||||
override def warn(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arguments: AnyRef*
|
||||
): Unit =
|
||||
log(LogLevel.Warning, format, arguments)
|
||||
|
||||
override def warn(@unused marker: Marker, msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Warning, msg, t)
|
||||
|
||||
override def isErrorEnabled: Boolean = isEnabled(LogLevel.Error)
|
||||
|
||||
override def error(msg: String): Unit = log(LogLevel.Error, msg)
|
||||
|
||||
override def error(format: String, arg: AnyRef): Unit =
|
||||
log(LogLevel.Error, format, arg)
|
||||
|
||||
override def error(format: String, arg1: AnyRef, arg2: AnyRef): Unit =
|
||||
log(LogLevel.Error, format, arg1, arg2)
|
||||
|
||||
override def error(format: String, arguments: AnyRef*): Unit =
|
||||
log(LogLevel.Error, format, arguments)
|
||||
|
||||
override def error(msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Error, msg, t)
|
||||
|
||||
override def isErrorEnabled(@unused marker: Marker): Boolean =
|
||||
isEnabled(LogLevel.Error)
|
||||
|
||||
override def error(@unused marker: Marker, msg: String): Unit =
|
||||
log(LogLevel.Error, msg)
|
||||
|
||||
override def error(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg: AnyRef
|
||||
): Unit =
|
||||
log(LogLevel.Error, format, arg)
|
||||
|
||||
override def error(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arg1: AnyRef,
|
||||
arg2: AnyRef
|
||||
): Unit = log(LogLevel.Error, format, arg1, arg2)
|
||||
|
||||
override def error(
|
||||
@unused marker: Marker,
|
||||
format: String,
|
||||
arguments: AnyRef*
|
||||
): Unit =
|
||||
log(LogLevel.Error, format, arguments)
|
||||
|
||||
override def error(@unused marker: Marker, msg: String, t: Throwable): Unit =
|
||||
log(LogLevel.Error, msg, t)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import org.slf4j.{ILoggerFactory, Logger => SLF4JLogger}
|
||||
|
||||
/**
|
||||
* A [[ILoggerFactory]] instance for the SLF4J backend.
|
||||
*/
|
||||
class LoggerFactory extends ILoggerFactory {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def getLogger(name: String): SLF4JLogger = {
|
||||
loggers.getOrElseUpdate(
|
||||
name,
|
||||
new Logger(name, LoggingServiceManager.Connection)
|
||||
)
|
||||
}
|
||||
|
||||
private val loggers = scala.collection.concurrent.TrieMap[String, Logger]()
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import org.enso.loggingservice.printers.Printer
|
||||
|
||||
/**
|
||||
* Represents modes the logging service can be running in.
|
||||
*
|
||||
* @tparam InitializationResult type that is returned when
|
||||
* [[LoggingServiceManager]] sets up the given
|
||||
* mode
|
||||
*/
|
||||
sealed trait LoggerMode[InitializationResult]
|
||||
object LoggerMode {
|
||||
|
||||
/**
|
||||
* Forwards log messages to a logging service server.
|
||||
*
|
||||
* @param endpoint URI that is used to connect to the server via WebSockets
|
||||
*/
|
||||
case class Client(endpoint: Uri) extends LoggerMode[Unit]
|
||||
|
||||
/**
|
||||
* Starts gathering messages from this and other components.
|
||||
*
|
||||
* Its initialization returns a [[ServerBinding]] that can be used to connect
|
||||
* to the initialized server.
|
||||
*
|
||||
* @param printers a list of printers that process the incoming messages
|
||||
* @param port optional port to listen at, if not provided, the OS will
|
||||
* choose a default port; the chosen port can be extracted from
|
||||
* the [[ServerBinding]] that is returned when the service is
|
||||
* initialized
|
||||
* @param interface interface to listen at
|
||||
*/
|
||||
case class Server(
|
||||
printers: Seq[Printer],
|
||||
port: Option[Int] = None,
|
||||
interface: String = "localhost"
|
||||
) extends LoggerMode[ServerBinding]
|
||||
|
||||
/**
|
||||
* Processes log messages locally.
|
||||
*
|
||||
* @param printers a list of printers that process the incoming messages
|
||||
*/
|
||||
case class Local(printers: Seq[Printer]) extends LoggerMode[Unit]
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
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.printers.{Printer, StderrPrinter}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* Manages the logging service.
|
||||
*/
|
||||
object LoggingServiceManager {
|
||||
private val messageQueue = new BlockingConsumerMessageQueue()
|
||||
private var currentLevel: LogLevel = LogLevel.Trace
|
||||
|
||||
/**
|
||||
* The default [[LoggerConnection]] that should be used by all backends which
|
||||
* want to use the logging service.
|
||||
*/
|
||||
object Connection extends LoggerConnection {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def send(message: InternalLogMessage): Unit =
|
||||
messageQueue.send(Left(message))
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* The returned [[InitializationResult]] depends on the mode.
|
||||
*
|
||||
* It is important to note that any printers passed inside of `mode` are from
|
||||
* now on owned by the setup function and the created service, so if service
|
||||
* creation fails, they will be shutdown alongside service termination. Any
|
||||
* printers passed to this function must not be reused.
|
||||
*/
|
||||
def setup[InitializationResult](
|
||||
mode: LoggerMode[InitializationResult],
|
||||
logLevel: LogLevel
|
||||
): Future[InitializationResult] = {
|
||||
currentLevel = logLevel
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
Future(doSetup(mode, logLevel))
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the logging service if it was initialized or runs
|
||||
* [[handlePendingMessages]] to handle logs that would be dropped due to the
|
||||
* logging service never being initialized.
|
||||
*
|
||||
* This method is also called as a shutdown hook, but it is good to call it
|
||||
* before shutting down to ensure that everything has a chance to terminate
|
||||
* correctly before the application exits. It can be safely called multiple
|
||||
* times.
|
||||
*/
|
||||
def tearDown(): Unit = {
|
||||
val service = currentService.synchronized {
|
||||
val service = currentService
|
||||
currentService = None
|
||||
service
|
||||
}
|
||||
|
||||
service match {
|
||||
case Some(running) => running.terminate()
|
||||
case None =>
|
||||
}
|
||||
|
||||
handlePendingMessages()
|
||||
}
|
||||
|
||||
Runtime.getRuntime.addShutdownHook(new Thread(() => tearDown()))
|
||||
|
||||
/**
|
||||
* Terminates the currently running logging service (if any) and replaces it
|
||||
* with a fallback logging service.
|
||||
*
|
||||
* Can be used if the currently logging service fails after initialization
|
||||
* and has to be shutdown.
|
||||
*/
|
||||
def replaceWithFallback(
|
||||
printers: Seq[Printer] = Seq(StderrPrinter.create())
|
||||
): Unit = {
|
||||
val fallback =
|
||||
Local.setup(currentLevel, messageQueue, printers)
|
||||
val previousService = currentService.synchronized {
|
||||
val previous = currentService
|
||||
currentService = Some(fallback)
|
||||
previous
|
||||
}
|
||||
|
||||
previousService match {
|
||||
case Some(service) =>
|
||||
service.terminate()
|
||||
case None =>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any pending logs (so that [[handlePendingMessages]] will not print
|
||||
* them).
|
||||
*
|
||||
* An internal method that is only used by [[TestLogger]].
|
||||
*/
|
||||
def dropPendingLogs(): Unit = messageQueue.drain(LogLevel.Off)
|
||||
|
||||
/**
|
||||
* Prints any messages that have been buffered but have not been logged yet
|
||||
* due to no loggers being active.
|
||||
*/
|
||||
private def handlePendingMessages(): Unit = {
|
||||
val danglingMessages = messageQueue.drain(currentLevel)
|
||||
if (danglingMessages.nonEmpty) {
|
||||
InternalLogger.error(
|
||||
"It seems that the logging service was never set up, " +
|
||||
"or log messages were reported after it has been terminated. " +
|
||||
"These messages are printed below:"
|
||||
)
|
||||
val stderrPrinter = StderrPrinter.create()
|
||||
danglingMessages.foreach { message =>
|
||||
stderrPrinter.print(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def doSetup[InitializationResult](
|
||||
mode: LoggerMode[InitializationResult],
|
||||
logLevel: LogLevel
|
||||
): InitializationResult = {
|
||||
currentService.synchronized {
|
||||
if (currentService.isDefined) {
|
||||
throw new IllegalStateException(
|
||||
"The logging service has already been set up."
|
||||
)
|
||||
}
|
||||
|
||||
val (service, result): (Service, InitializationResult) = mode match {
|
||||
case LoggerMode.Client(endpoint) =>
|
||||
(Client.setup(endpoint, messageQueue, logLevel), ())
|
||||
case LoggerMode.Server(printers, port, interface) =>
|
||||
val server = Server.setup(
|
||||
interface,
|
||||
port.getOrElse(0),
|
||||
messageQueue,
|
||||
printers,
|
||||
logLevel
|
||||
)
|
||||
(server, server.getBinding())
|
||||
case LoggerMode.Local(printers) =>
|
||||
(Local.setup(logLevel, messageQueue, printers), ())
|
||||
}
|
||||
currentService = Some(service)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import akka.http.scaladsl.model.Uri.{Authority, Host, Path}
|
||||
|
||||
case class ServerBinding(port: Int) {
|
||||
def toUri(host: String = "localhost"): Uri =
|
||||
Uri(
|
||||
scheme = "ws",
|
||||
authority = Authority(host = Host(host), port = port),
|
||||
path = Path./
|
||||
)
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package org.enso.loggingservice
|
||||
|
||||
import org.enso.loggingservice.printers.TestPrinter
|
||||
|
||||
/**
|
||||
* A helper object for handling logs in tests.
|
||||
*/
|
||||
object TestLogger {
|
||||
|
||||
/**
|
||||
* A log message returned by [[gatherLogs]].
|
||||
*
|
||||
* It contains the loglevel and message, but ignores attached exceptions.
|
||||
*/
|
||||
case class TestLogMessage(logLevel: LogLevel, message: String)
|
||||
|
||||
/**
|
||||
* Gathers logs logged during execution of `action`.
|
||||
*
|
||||
* This method should be used only inside of tests. Any tests using it should
|
||||
* be ran with `parallelExecution` set to false, as global logger state has
|
||||
* to be modified to gather the logs.
|
||||
*/
|
||||
def gatherLogs(action: => Unit): Seq[TestLogMessage] = {
|
||||
LoggingServiceManager.dropPendingLogs()
|
||||
LoggingServiceManager.tearDown()
|
||||
val printer = new TestPrinter
|
||||
LoggingServiceManager.setup(
|
||||
LoggerMode.Local(Seq(printer)),
|
||||
LogLevel.Debug
|
||||
)
|
||||
action
|
||||
LoggingServiceManager.tearDown()
|
||||
printer.getLoggedMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops any logs that are pending due to the logging service not being set
|
||||
* up.
|
||||
*
|
||||
* This method should be used only inside of tests. Any tests using it should
|
||||
* be ran with `parallelExecution` set to false, as global logger state has
|
||||
* to be modified to gather the logs.
|
||||
*/
|
||||
def dropLogs(): Unit = {
|
||||
LoggingServiceManager.dropPendingLogs()
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.enso.loggingservice.internal
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
import scala.io.AnsiColor
|
||||
|
||||
/**
|
||||
* Renders log messages in the same way as [[DefaultLogMessageRenderer]] but
|
||||
* adds ANSI escape codes to display the log level in color.
|
||||
*/
|
||||
class ANSIColorsMessageRenderer(printExceptions: Boolean)
|
||||
extends DefaultLogMessageRenderer(printExceptions) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def renderLevel(logLevel: LogLevel): String = {
|
||||
val color = logLevel match {
|
||||
case LogLevel.Error => Some(AnsiColor.RED)
|
||||
case LogLevel.Warning => Some(AnsiColor.YELLOW)
|
||||
case LogLevel.Debug => Some(AnsiColor.CYAN)
|
||||
case LogLevel.Trace => Some(AnsiColor.CYAN)
|
||||
case _ => None
|
||||
}
|
||||
color match {
|
||||
case Some(ansiColor) =>
|
||||
s"$ansiColor${super.renderLevel(logLevel)}${AnsiColor.RESET}"
|
||||
case None => super.renderLevel(logLevel)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package org.enso.loggingservice.internal
|
||||
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import org.graalvm.nativeimage.ImageInfo
|
||||
|
||||
/**
|
||||
* Handles VT-compatible color output in the terminal.
|
||||
*/
|
||||
object AnsiTerminal {
|
||||
|
||||
/**
|
||||
* Tries enabling ANSI colors in terminal output and returns true if it
|
||||
* succeeded.
|
||||
*
|
||||
* We assume that ANSI colors are supported by default on UNIX platforms. On
|
||||
* Windows, we use native calls to enable them, currently this is only
|
||||
* supported in native-image builds. Currently ANSI colors are not supported
|
||||
* on non-native Windows targets.
|
||||
*/
|
||||
def tryEnabling(): Boolean = {
|
||||
if (isWindows) {
|
||||
if (ImageInfo.inImageCode) {
|
||||
try {
|
||||
NativeAnsiTerm.enableVT()
|
||||
true
|
||||
} catch {
|
||||
case error: RuntimeException =>
|
||||
Logger[AnsiTerminal.type].warn(
|
||||
s"Failed to initialize VT terminal (output will not contain " +
|
||||
s"colors): ${error.getMessage}"
|
||||
)
|
||||
false
|
||||
}
|
||||
} else false
|
||||
} else true
|
||||
}
|
||||
|
||||
private def isWindows: Boolean =
|
||||
System.getProperty("os.name").toLowerCase.contains("win")
|
||||
|
||||
/**
|
||||
* Checks if output of this program may be piped.
|
||||
*/
|
||||
def isLikelyPiped: Boolean = System.console() == null
|
||||
|
||||
/**
|
||||
* Checks if the output is connected to a terminal that can handle color
|
||||
* output.
|
||||
*
|
||||
* On Windows, this function also enables color output, so any code that
|
||||
* wants to use VT escape codes for colors (and is not assuming that its
|
||||
* output is redirected) should first call this function to try enabling it
|
||||
* and only use them if this function returned true.
|
||||
*/
|
||||
def canUseColors(): Boolean = {
|
||||
!isLikelyPiped && AnsiTerminal.tryEnabling()
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.enso.loggingservice.internal
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
import org.enso.loggingservice.LogLevel
|
||||
|
||||
/**
|
||||
* A base type for log messages parametrized by the exception representation.
|
||||
*/
|
||||
trait BaseLogMessage[ExceptionType] {
|
||||
def level: LogLevel
|
||||
def timestamp: Instant
|
||||
def group: String
|
||||
def message: String
|
||||
def exception: Option[ExceptionType]
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
package org.enso.loggingservice.internal
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue
|
||||
|
||||
import org.enso.loggingservice.LogLevel
|
||||
import org.enso.loggingservice.internal.protocol.WSLogMessage
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/**
|
||||
* A message queue that can be consumed by a thread in a loop with a limited
|
||||
* buffer.
|
||||
*/
|
||||
class BlockingConsumerMessageQueue(bufferSize: Int = 5000) {
|
||||
|
||||
/**
|
||||
* Enqueues the `message` to be sent and returns immediately.
|
||||
*
|
||||
* If any underlying buffers are full, they may be removed and a warning will
|
||||
* be issued.
|
||||
*/
|
||||
def send(message: Either[InternalLogMessage, WSLogMessage]): Unit = {
|
||||
val inserted = queue.offer(message)
|
||||
if (!inserted) {
|
||||
queue.clear()
|
||||
queue.offer(Left(queueOverflowMessage))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next message in the queue, skipping messages that should be
|
||||
* ignored and waiting if no messages are currently available.
|
||||
*
|
||||
* The distinction between internal and external messages is that internal
|
||||
* messages should only be considered if they have log level that is enabled.
|
||||
* However, all external log messages should be processed, regardless of
|
||||
* their log level, because external messages come from other components
|
||||
* whose log level is set independently.
|
||||
*/
|
||||
@tailrec
|
||||
final def nextMessage(internalLogLevel: LogLevel): WSLogMessage = {
|
||||
val (message, internal) = encodeMessage(queue.take())
|
||||
if (isMessageRelevant(internalLogLevel)(message, internal))
|
||||
message
|
||||
else nextMessage(internalLogLevel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all currently enqueued messages, skipping ones that should be
|
||||
* ignored.
|
||||
*
|
||||
* See [[nextMessage]] for explanation which messages are ignored.
|
||||
*/
|
||||
def drain(internalLogLevel: LogLevel): Seq[WSLogMessage] = {
|
||||
val buffer = scala.collection.mutable
|
||||
.Buffer[Either[InternalLogMessage, WSLogMessage]]()
|
||||
import scala.jdk.CollectionConverters._
|
||||
queue.drainTo(buffer.asJava)
|
||||
buffer.toSeq
|
||||
.map(encodeMessage)
|
||||
.filter((isMessageRelevant(internalLogLevel) _).tupled)
|
||||
.map(_._1)
|
||||
}
|
||||
|
||||
/**
|
||||
* All external messages are relevant, but internal messages relevancy depends
|
||||
* on its log level.
|
||||
*/
|
||||
private def isMessageRelevant(
|
||||
internalLogLevel: LogLevel
|
||||
)(message: WSLogMessage, internal: Boolean): Boolean =
|
||||
!internal || internalLogLevel.shouldLog(message.level)
|
||||
|
||||
/**
|
||||
* Returns the encoded message and a boolean value indicating if it was
|
||||
* internal.
|
||||
*/
|
||||
private def encodeMessage(
|
||||
message: Either[InternalLogMessage, WSLogMessage]
|
||||
): (WSLogMessage, Boolean) =
|
||||
message.fold(msg => (msg.toLogMessage, true), (_, false))
|
||||
|
||||
private val queueOverflowMessage: InternalLogMessage =
|
||||
InternalLogMessage(
|
||||
level = LogLevel.Warning,
|
||||
classOf[BlockingConsumerMessageQueue].getCanonicalName,
|
||||
"The Logger does not keep up with processing log messages. " +
|
||||
"Some log messages have been dropped.",
|
||||
None
|
||||
)
|
||||
|
||||
private val queue =
|
||||
new ArrayBlockingQueue[Either[InternalLogMessage, WSLogMessage]](bufferSize)
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package org.enso.loggingservice.internal
|
||||
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.{Instant, ZoneOffset, ZonedDateTime}
|
||||
|
||||
import org.enso.loggingservice.LogLevel
|
||||
import org.enso.loggingservice.internal.protocol.{
|
||||
SerializedException,
|
||||
WSLogMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the log message using the default format, including attached
|
||||
* exceptions if [[printExceptions]] is set.
|
||||
*/
|
||||
class DefaultLogMessageRenderer(printExceptions: Boolean)
|
||||
extends LogMessageRenderer {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def render(logMessage: WSLogMessage): String = {
|
||||
val level = renderLevel(logMessage.level)
|
||||
val timestamp = renderTimestamp(logMessage.timestamp)
|
||||
val base =
|
||||
s"[$level] [$timestamp] [${logMessage.group}] ${logMessage.message}"
|
||||
addException(base, logMessage.exception)
|
||||
}
|
||||
|
||||
private val timestampZone = ZoneOffset.UTC
|
||||
|
||||
/**
|
||||
* Renders the timestamp.
|
||||
*/
|
||||
def renderTimestamp(timestamp: Instant): String =
|
||||
ZonedDateTime
|
||||
.ofInstant(timestamp, timestampZone)
|
||||
.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
|
||||
|
||||
/**
|
||||
* Adds attached exception's stack trace (if available) to the log if
|
||||
* printing stack traces is enabled.
|
||||
*/
|
||||
def addException(
|
||||
message: String,
|
||||
exception: Option[SerializedException]
|
||||
): String =
|
||||
exception match {
|
||||
case Some(e) if printExceptions =>
|
||||
message + "\n" + renderException(e)
|
||||
case _ => message
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an exception with its strack trace.
|
||||
*/
|
||||
def renderException(exception: SerializedException): String = {
|
||||
val head = s"${exception.name}: ${exception.message}"
|
||||
val trace = exception.stackTrace.map(elem =>
|
||||
s" at ${elem.element}(${elem.location})"
|
||||
)
|
||||
val cause = exception.cause
|
||||
.map(e => s"\nCaused by: ${renderException(e)}")
|
||||
.getOrElse("")
|
||||
head + trace.map("\n" + _).mkString + cause
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a log level.
|
||||
*/
|
||||
def renderLevel(logLevel: LogLevel): String =
|
||||
logLevel match {
|
||||
case LogLevel.Error => "error"
|
||||
case LogLevel.Warning => "warn"
|
||||
case LogLevel.Info => "info"
|
||||
case LogLevel.Debug => "debug"
|
||||
case LogLevel.Trace => "trace"
|
||||
case LogLevel.Off => "off"
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package org.enso.loggingservice.internal
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
import org.enso.loggingservice.LogLevel
|
||||
import org.enso.loggingservice.internal.protocol.{
|
||||
SerializedException,
|
||||
WSLogMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal log message that is used for local logging.
|
||||
*
|
||||
* @param level log level
|
||||
* @param timestamp timestamp indicating when the message was created
|
||||
* @param group group associated with the message
|
||||
* @param message text message
|
||||
* @param exception optional attached exception
|
||||
*/
|
||||
case class InternalLogMessage(
|
||||
level: LogLevel,
|
||||
timestamp: Instant,
|
||||
group: String,
|
||||
message: String,
|
||||
exception: Option[Throwable]
|
||||
) extends BaseLogMessage[Throwable] {
|
||||
|
||||
/**
|
||||
* Converts to [[WSLogMessage]] by serializing the attached exception.
|
||||
*/
|
||||
def toLogMessage: WSLogMessage =
|
||||
WSLogMessage(
|
||||
level = level,
|
||||
timestamp = timestamp.truncatedTo(ChronoUnit.MILLIS),
|
||||
group = group,
|
||||
message = message,
|
||||
exception = exception.map(SerializedException.fromException)
|
||||
)
|
||||
}
|
||||
|
||||
object InternalLogMessage {
|
||||
|
||||
/**
|
||||
* Creates a log message with the timestamp set to the current instant.
|
||||
*/
|
||||
def apply(
|
||||
level: LogLevel,
|
||||
group: String,
|
||||
message: String,
|
||||
exception: Option[Throwable]
|
||||
): InternalLogMessage =
|
||||
InternalLogMessage(
|
||||
level = level,
|
||||
timestamp = Instant.now(),
|
||||
group = group,
|
||||
message = message,
|
||||
exception = exception
|
||||
)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.enso.loggingservice.internal
|
||||
|
||||
/**
|
||||
* An internal logger used for reporting errors within the logging service
|
||||
* itself.
|
||||
*
|
||||
* As the logging service cannot be used to report its own errors (because a
|
||||
* logging service error likely means that it is in a unusable state), its
|
||||
* errors are printed to the standard error output.
|
||||
*/
|
||||
object InternalLogger {
|
||||
|
||||
/**
|
||||
* Reports an internal logging service error with the given message.
|
||||
*/
|
||||
def error(message: String): Unit = {
|
||||
System.err.println(s"[internal-logger-error] $message")
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user