Logging Service in the Launcher (#1169)

Migrate launcher's HTTP backend from Apache HTTP to Akka.
This commit is contained in:
Radosław Waśko 2020-10-02 18:17:21 +02:00 committed by GitHub
parent 6c409958ec
commit c824c1cb7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 6111 additions and 797 deletions

View File

@ -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.
*/

View File

@ -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.

View 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.

View 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

View File

@ -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.

View 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.

View File

@ -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.

View File

@ -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"]
}
]
}
]

View File

@ -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": []
}

View File

@ -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();
}
}

View File

@ -0,0 +1,6 @@
akka {
loggers = ["akka.event.slf4j.Slf4jLogger"]
logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
version = "2.6.6"
stdout-loglevel = "ERROR"
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
)
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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 " +

View File

@ -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."

View File

@ -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.")
)
}
}

View File

@ -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)
}
}

View File

@ -184,7 +184,7 @@ object InternalOpts {
LauncherUpgrader
.default(config, originalExecutablePath = originalPath)
.internalContinueUpgrade(version)
sys.exit(0)
Main.exit(0)
}
}
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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."
)

View File

@ -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.!!
}

View File

@ -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
)

View File

@ -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()
}
}
}

View File

@ -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."
)

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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
)
}

View File

@ -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
}

View File

@ -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 " +

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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. " +

View File

@ -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()
}

View File

@ -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."

View File

@ -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 {

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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(),

View File

@ -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

View File

@ -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 =

View File

@ -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 {

View File

@ -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)
}

View File

@ -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,

View File

@ -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) {

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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()
}

View File

@ -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;
/**

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1 @@
Args = -H:ReflectionConfigurationResources=${.}/reflect-config.json

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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 =>

View File

@ -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: " +

View File

@ -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)
}
}
}

View File

@ -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()

View File

@ -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)

View File

@ -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] =

View File

@ -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)
}

View File

@ -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] =

View File

@ -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)

View File

@ -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

View File

@ -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() {
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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)
)
}
}
}

View File

@ -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)
}

View File

@ -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]()
}

View File

@ -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]
}

View File

@ -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
}
}
}

View File

@ -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./
)
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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]
}

View File

@ -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)
}

View File

@ -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"
}
}

View File

@ -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
)
}

View File

@ -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