in sources/amper-cli/src/org/jetbrains/amper/cli/commands/UpdateCommand.kt [91:155]
override suspend fun run() {
// We could in theory find the parent dir of the actual script that launched Amper (even without project root
// discovery, by passing more info from the wrapper to Amper), but the benefit would be marginal, and it would
// break amper-from-sources.
// Also, we would still have to respect an explicit --root option to allow users to update other projects.
val targetDir = commonOptions.explicitProjectRoot ?: Path(".")
val amperBashPath = targetDir.resolve("amper")
val amperBatPath = targetDir.resolve("amper.bat")
checkNotDirectories(amperBashPath, amperBatPath)
if (!create) {
confirmCreateIfMissingWrappers(amperBashPath, amperBatPath)
}
val version = desiredVersion.resolve()
terminal.println("Downloading Amper scripts...")
val newBashPath = downloadWrapper(version = version, extension = "").apply { setReadExecPermissions() }
val newBatPath = downloadWrapper(version = version, extension = ".bat").apply { setReadExecPermissions() }
terminal.println("Download complete.")
if (amperBashPath.exists() && newBashPath.readText() == amperBashPath.readText() &&
amperBatPath.exists() && newBatPath.readText() == amperBatPath.readText()) {
terminal.println("Amper is already in version $version, nothing to update")
return
}
// Test the new script and download the Amper distribution and JRE
val exitCode = spanBuilder("New version first run").use {
runAmperVersionFirstRun(newBatPath, newBashPath)
}
if (exitCode != 0) {
userReadableError("Couldn't run the new Amper version. Please check the errors above.")
}
// Replacing a bash script while it's running is possible. We use move commands to ensure the physical file on
// disk is not modified, thus we can write a new physical file to the old location. Bash will keep loading the
// old file incrementally from the old physical file using its old file descriptor, which is good.
spanBuilder("Replace 'amper' script (bash)").use {
copyAndReplaceSafely(source = newBashPath, target = amperBashPath)
}
// Batch files are different. When running, cmd.exe reloads the file after each command and tries to resume at
// whatever byte offset it was. We can modify the file while it's running, but when the java command running
// this code completes, it will resume in the new wrapper code. If the new script is shorter, cmd.exe will just
// stop and the command completes normally. If the new script is longer, then cmd.exe will likely resume in the
// middle of a command in the middle of the script, which will fail miserably.
// Even with atomic moves, the new file is reloaded, so in this case we have to spawn a process that will
// replace the old wrapper after the update command (and the current wrapper) finished executing.
// The offset of the exit command is where the script would normally resume (given the characters we use in our
// scripts, the UTF-8 byte offset should correspond to the character offset).
val runningWrapperResumeOffset = runningWrapper.readText().lastIndexOf("exit /B %ERRORLEVEL%")
val batUpdateInPlaceWouldBreak = amperBatPath.exists()
&& amperBatPath.isSameFileAs(runningWrapper)
&& newBatPath.fileSize() > runningWrapperResumeOffset
spanBuilder("Replace 'amper.bat' script").use { span ->
if (batUpdateInPlaceWouldBreak) {
copyAndReplaceLaterWindows(source = newBatPath, target = amperBatPath)
span.addEvent("amper.bat script copy scheduled for after JVM shutdown")
} else {
copyAndReplaceSafely(source = newBatPath, target = amperBatPath)
}
}
terminal.success("Update successful")
}