mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 21:27:15 +03:00
Handling Broken Releases in the Launcher (#1113)
This commit is contained in:
parent
4e337840cf
commit
eb208301db
@ -115,6 +115,10 @@ It can also contain the following additional fields:
|
||||
of the engine package that is being launched. Optionally, the option may
|
||||
define `os` which will restrict this option only to the provided operating
|
||||
system. Possible `os` values are `linux`, `macos` and `windows`.
|
||||
- `broken` - can be set to `true` to mark this release as broken. This field is
|
||||
never set in a release. Instead, when the launcher is installing a release
|
||||
marked as broken using the `broken` file, it adds this property to the
|
||||
manifest to preserve that information.
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -57,7 +57,8 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
*/
|
||||
def listEngines(): Unit = {
|
||||
for (engine <- componentsManager.listInstalledEngines()) {
|
||||
println(engine.version.toString)
|
||||
val broken = if (engine.isMarkedBroken) " (broken)" else ""
|
||||
println(engine.version.toString + broken)
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +87,8 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
val runtimeName = runtime
|
||||
.map(_.toString)
|
||||
.getOrElse("no runtime found for this distribution")
|
||||
println(s"Enso ${engine.version} -> $runtimeName")
|
||||
val broken = if (engine.isMarkedBroken) " (broken)" else ""
|
||||
println(s"Enso ${engine.version}$broken -> $runtimeName")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,17 +10,23 @@ import java.io.PrintStream
|
||||
* is implemented in #1031
|
||||
*/
|
||||
object Logger {
|
||||
private case class Level(name: String, level: Int, stream: PrintStream)
|
||||
private val Debug = Level("debug", 1, System.err)
|
||||
private val Info = Level("info", 2, System.out)
|
||||
private val Warning = Level("warn", 3, System.err)
|
||||
private val Error = Level("error", 4, System.err)
|
||||
|
||||
// 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) {
|
||||
level.stream.println(s"[${level.name}] $msg")
|
||||
level.stream.flush()
|
||||
val stream = level.stream()
|
||||
stream.println(s"[${level.name}] $msg")
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.nio.file.{Files, Path, StandardOpenOption}
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.CLIOutput
|
||||
@ -16,7 +16,7 @@ import org.enso.launcher.releases.{
|
||||
import org.enso.launcher.{FileSystem, Launcher, Logger}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import scala.util.{Failure, Success, Try, Using}
|
||||
|
||||
/**
|
||||
* Manages runtime and engine components.
|
||||
@ -129,10 +129,20 @@ class ComponentsManager(
|
||||
* installed.
|
||||
*
|
||||
* Any other errors regarding loading the engine are thrown.
|
||||
* If the engine is marked as broken, a warning is reported.
|
||||
*/
|
||||
def findEngine(version: SemVer): Option[Engine] =
|
||||
getEngine(version)
|
||||
.map(Some(_))
|
||||
.map { engine =>
|
||||
if (engine.isMarkedBroken) {
|
||||
Logger.warn(
|
||||
s"Running an engine release ($version) that is marked as broken. " +
|
||||
s"Please consider upgrading to a stable release."
|
||||
)
|
||||
}
|
||||
|
||||
Some(engine)
|
||||
}
|
||||
.recoverWith {
|
||||
case _: ComponentMissingError => Success(None)
|
||||
case e: LauncherUpgradeRequiredError => Failure(e)
|
||||
@ -198,7 +208,6 @@ class ComponentsManager(
|
||||
* @return
|
||||
*/
|
||||
def listInstalledEngines(): Seq[Engine] = {
|
||||
|
||||
FileSystem
|
||||
.listDirectory(distributionManager.paths.engines)
|
||||
.map(path => (path, loadEngine(path)))
|
||||
@ -258,6 +267,31 @@ class ComponentsManager(
|
||||
*/
|
||||
private def installEngine(version: SemVer): Engine = {
|
||||
val engineRelease = engineReleaseProvider.getRelease(version).get
|
||||
if (engineRelease.isBroken) {
|
||||
if (cliOptions.autoConfirm) {
|
||||
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(
|
||||
s"The engine release $version is marked as broken and it should " +
|
||||
s"not be used."
|
||||
)
|
||||
val continue = CLIOutput.askConfirmation(
|
||||
"Are you sure you still want to continue installing this version " +
|
||||
"despite the warning?"
|
||||
)
|
||||
if (!continue) {
|
||||
throw InstallationError(
|
||||
"Installation has been cancelled by the user because the " +
|
||||
"requested engine release is marked as broken."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FileSystem.withTemporaryDirectory("enso-install") { directory =>
|
||||
Logger.debug(s"Downloading packages to $directory")
|
||||
val enginePackage = directory / engineRelease.packageFileName
|
||||
@ -288,6 +322,29 @@ class ComponentsManager(
|
||||
}
|
||||
}
|
||||
|
||||
if (engineRelease.isBroken) {
|
||||
try {
|
||||
Using(
|
||||
Files.newBufferedWriter(
|
||||
engineTemporaryPath / Manifest.DEFAULT_MANIFEST_NAME,
|
||||
StandardOpenOption.WRITE,
|
||||
StandardOpenOption.APPEND
|
||||
)
|
||||
) { writer =>
|
||||
writer.newLine()
|
||||
writer.write(s"${Manifest.Fields.brokenMark}: true\n")
|
||||
}.get
|
||||
} catch {
|
||||
case ex: Exception =>
|
||||
undoTemporaryEngine()
|
||||
throw InstallationError(
|
||||
"Cannot add the broken mark to the installed engine's " +
|
||||
"manifest. The installation has failed.",
|
||||
ex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val temporaryEngine = loadEngine(engineTemporaryPath).getOrElse {
|
||||
undoTemporaryEngine()
|
||||
throw InstallationError(
|
||||
@ -296,7 +353,9 @@ class ComponentsManager(
|
||||
}
|
||||
|
||||
try {
|
||||
if (temporaryEngine.manifest != engineRelease.manifest) {
|
||||
val patchedReleaseManifest =
|
||||
engineRelease.manifest.copy(brokenMark = engineRelease.isBroken)
|
||||
if (temporaryEngine.manifest != patchedReleaseManifest) {
|
||||
undoTemporaryEngine()
|
||||
throw InstallationError(
|
||||
"Manifest of installed engine does not match the published " +
|
||||
|
@ -17,8 +17,10 @@ case class Engine(version: SemVer, path: Path, manifest: Manifest) {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"Enso Engine $version"
|
||||
override def toString: String = {
|
||||
val broken = if (isMarkedBroken) " (marked as broken)" else ""
|
||||
s"Enso Engine $version$broken"
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime version that is associated with this engine and should be used
|
||||
@ -46,4 +48,13 @@ case class Engine(version: SemVer, path: Path, manifest: Manifest) {
|
||||
*/
|
||||
def isValid: Boolean =
|
||||
Files.exists(runnerPath) && Files.exists(runtimePath)
|
||||
|
||||
/**
|
||||
* Returns if the engine release was marked as broken when it was being
|
||||
* installed.
|
||||
*
|
||||
* Releases marked as broken are not considered when looking for the latest
|
||||
* installed release and issue a warning when they are used.
|
||||
*/
|
||||
def isMarkedBroken: Boolean = manifest.brokenMark
|
||||
}
|
||||
|
@ -31,7 +31,8 @@ case class Manifest(
|
||||
minimumLauncherVersion: SemVer,
|
||||
graalVMVersion: SemVer,
|
||||
graalJavaVersion: String,
|
||||
jvmOptions: Seq[JVMOption]
|
||||
jvmOptions: Seq[JVMOption],
|
||||
brokenMark: Boolean
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -173,11 +174,12 @@ object Manifest {
|
||||
}
|
||||
}
|
||||
|
||||
private object Fields {
|
||||
object Fields {
|
||||
val minimumLauncherVersion = "minimum-launcher-version"
|
||||
val jvmOptions = "jvm-options"
|
||||
val graalVMVersion = "graal-vm-version"
|
||||
val graalJavaVersion = "graal-java-version"
|
||||
val brokenMark = "broken"
|
||||
}
|
||||
|
||||
implicit private val decoder: Decoder[Manifest] = { json =>
|
||||
@ -189,11 +191,13 @@ object Manifest {
|
||||
.get[String](Fields.graalJavaVersion)
|
||||
.orElse(json.get[Int](Fields.graalJavaVersion).map(_.toString))
|
||||
jvmOptions <- json.getOrElse[Seq[JVMOption]](Fields.jvmOptions)(Seq())
|
||||
broken <- json.getOrElse[Boolean](Fields.brokenMark)(false)
|
||||
} yield Manifest(
|
||||
minimumLauncherVersion = minimumLauncherVersion,
|
||||
graalVMVersion = graalVMVersion,
|
||||
graalJavaVersion = graalJavaVersion,
|
||||
jvmOptions = jvmOptions
|
||||
jvmOptions = jvmOptions,
|
||||
brokenMark = broken
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ class GlobalConfigurationManager(
|
||||
val latestInstalled =
|
||||
componentsManager
|
||||
.listInstalledEngines()
|
||||
.filter(!_.isMarkedBroken)
|
||||
.map(_.version)
|
||||
.sorted
|
||||
.lastOption
|
||||
|
@ -17,11 +17,13 @@ import scala.util.{Failure, Success, Try}
|
||||
*
|
||||
* @param version engine version
|
||||
* @param manifest manifest associated with the release
|
||||
* @param isBroken specifies whether this release is marked as broken
|
||||
* @param release a [[Release]] that allows to download assets
|
||||
*/
|
||||
case class EngineRelease(
|
||||
version: SemVer,
|
||||
manifest: Manifest,
|
||||
isBroken: Boolean,
|
||||
release: Release
|
||||
) {
|
||||
|
||||
@ -55,11 +57,17 @@ class EngineReleaseProvider(releaseProvider: ReleaseProvider) {
|
||||
|
||||
/**
|
||||
* Returns the version of the most recent engine release.
|
||||
*
|
||||
* It ignores releases marked as broken, so the latest non-broken release is
|
||||
* returned.
|
||||
*/
|
||||
def findLatest(): Try[SemVer] =
|
||||
releaseProvider.listReleases().flatMap { releases =>
|
||||
val versions =
|
||||
releases.map(_.tag.stripPrefix(tagPrefix)).flatMap(SemVer(_))
|
||||
releases
|
||||
.filter(!isBroken(_))
|
||||
.map(_.tag.stripPrefix(tagPrefix))
|
||||
.flatMap(SemVer(_))
|
||||
versions.sorted.lastOption.map(Success(_)).getOrElse {
|
||||
Failure(ReleaseProviderException("No valid engine versions were found"))
|
||||
}
|
||||
@ -94,9 +102,20 @@ class EngineReleaseProvider(releaseProvider: ReleaseProvider) {
|
||||
)
|
||||
)
|
||||
)
|
||||
} yield EngineRelease(version, manifest, release)
|
||||
} yield EngineRelease(
|
||||
version = version,
|
||||
isBroken = isBroken(release),
|
||||
manifest = manifest,
|
||||
release = release
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given release is broken.
|
||||
*/
|
||||
private def isBroken(release: Release): Boolean =
|
||||
release.assets.exists(_.fileName == "broken")
|
||||
|
||||
/**
|
||||
* Downloads the package associated with the given release into
|
||||
* `destination`.
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -2,11 +2,13 @@ package org.enso.launcher.components
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.config.GlobalConfigurationManager
|
||||
|
||||
class ComponentsManagerSpec extends ComponentsManagerTest {
|
||||
|
||||
"ComponentsManager" should {
|
||||
"find the latest engine version in semver ordering" in {
|
||||
"find the latest engine version in semver ordering " +
|
||||
"(skipping broken releases)" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
componentsManager.fetchLatestEngineVersion() shouldEqual SemVer(0, 0, 1)
|
||||
@ -66,6 +68,53 @@ class ComponentsManagerSpec extends ComponentsManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
"preserve the broken mark when installing a broken release" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val brokenVersion = SemVer(0, 1, 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."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"skip broken releases when finding latest installed version" in {
|
||||
Logger.suppressWarnings {
|
||||
val (distributionManager, componentsManager, _) = makeManagers()
|
||||
val configurationManager =
|
||||
new GlobalConfigurationManager(componentsManager, distributionManager)
|
||||
|
||||
val validVersion = SemVer(0, 0, 1)
|
||||
val newerButBrokenVersion = SemVer(0, 1, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(validVersion)
|
||||
componentsManager.findOrInstallEngine(newerButBrokenVersion)
|
||||
|
||||
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 brokenVersion = SemVer(0, 1, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(brokenVersion)
|
||||
|
||||
componentsManager.findEngine(brokenVersion).value
|
||||
}
|
||||
val stderr = stream.toString
|
||||
stderr should include("is marked as broken")
|
||||
stderr should include("consider upgrading")
|
||||
}
|
||||
|
||||
"uninstall the runtime iff it is not used by any engines" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
|
Loading…
Reference in New Issue
Block a user