Handling Broken Releases in the Launcher (#1113)

This commit is contained in:
Radosław Waśko 2020-09-01 12:03:48 +02:00 committed by GitHub
parent 4e337840cf
commit eb208301db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 192 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ class GlobalConfigurationManager(
val latestInstalled =
componentsManager
.listInstalledEngines()
.filter(!_.isMarkedBroken)
.map(_.version)
.sorted
.lastOption

View File

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

View File

@ -0,0 +1,3 @@
minimum-launcher-version: 0.0.1
graal-vm-version: 2.0.0
graal-java-version: 11

View File

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