From f6cf6f3683238e793eff9fb85de93bc6bbadb98a Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 20 Aug 2026 11:10:41 +0200 Subject: [PATCH 1/2] Fix Grill failure exit codes --- src/main/kotlin/config/WurstProjectConfig.kt | 6 +- src/main/kotlin/file/SetupApp.kt | 98 +++++++++++++------ src/main/kotlin/global/InstallationManager.kt | 63 +++++++----- src/test/kotlin/CMDTests.kt | 33 +++++++ 4 files changed, 141 insertions(+), 59 deletions(-) diff --git a/src/main/kotlin/config/WurstProjectConfig.kt b/src/main/kotlin/config/WurstProjectConfig.kt index 699d418..3d7d88b 100644 --- a/src/main/kotlin/config/WurstProjectConfig.kt +++ b/src/main/kotlin/config/WurstProjectConfig.kt @@ -63,8 +63,8 @@ object WurstProjectConfig { private fun createProject(projectRoot: Path, gameRoot: Path?, projectConfig: WurstProjectConfigData, templateBranch: String) { Log.print("Creating project root..") if (Files.exists(projectRoot) && Files.list(projectRoot).filter { !Files.isDirectory(it) }.findAny().isPresent) { - log.error("Project root already exists and contains files") - Log.print("\nError: Project root already exists!\n") + log.error("āŒ Project root already exists and contains files.") + ExitHandler.exit(1) } else { Files.createDirectories(projectRoot) Log.print("done\n") @@ -90,6 +90,7 @@ object WurstProjectConfig { } else { Log.print("error\n") log.error("āŒ Cannot extract template files. Close any Wurst, VSCode or Eclipse instances and try again.") + ExitHandler.exit(1) } setupEnvironment(projectRoot, gameRoot, projectConfig) @@ -230,4 +231,3 @@ object WurstProjectConfig { " },\n" + "\t\"search.useIgnoreFiles\": false }" } - diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index c1ecc58..65b6112 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -231,18 +231,22 @@ object SetupApp { } setup.command == CLICommand.TEST -> { progress("āš—ļø Running tests...") - if (InstallationManager.status != InstallationManager.InstallationStatus.NOT_INSTALLED && configData != null) { - testProject(configData) - } else if (configData == null) { + if (configData == null) { missingProject() + } else if (InstallationManager.status == InstallationManager.InstallationStatus.NOT_INSTALLED) { + missingCompiler() + } else { + testProject(configData) } } setup.command == CLICommand.TYPECHECK -> { progress("šŸ” Typechecking project...") - if (InstallationManager.status != InstallationManager.InstallationStatus.NOT_INSTALLED && configData != null) { - typecheckProject(configData) - } else if (configData == null) { + if (configData == null) { missingProject() + } else if (InstallationManager.status == InstallationManager.InstallationStatus.NOT_INSTALLED) { + missingCompiler() + } else { + typecheckProject(configData) } } setup.command == CLICommand.OUTDATED -> { @@ -266,11 +270,13 @@ object SetupApp { if (mapArg != null) { if (!Files.exists(setup.projectRoot.resolve(mapArg))) { missingMap(mapArg) - } else if (InstallationManager.status != InstallationManager.InstallationStatus.NOT_INSTALLED && configData != null) { - setup.commandArg = mapArg - buildProject(configData) } else if (configData == null) { missingProject() + } else if (InstallationManager.status == InstallationManager.InstallationStatus.NOT_INSTALLED) { + missingCompiler() + } else { + setup.commandArg = mapArg + buildProject(configData) } } } @@ -288,7 +294,9 @@ object SetupApp { val mapPath = setup.projectRoot.resolve(mapArg) if (!Files.exists(mapPath)) { missingMap(mapArg) - } else if (InstallationManager.status != InstallationManager.InstallationStatus.NOT_INSTALLED) { + } else if (InstallationManager.status == InstallationManager.InstallationStatus.NOT_INSTALLED) { + missingCompiler() + } else { exportObjects(mapPath) } } @@ -300,9 +308,13 @@ object SetupApp { InstallationManager.ensureGrillJarInstalled() ExitHandler.exit(0) } catch(e: Exception) { - log.error("Grill update failed. Original files might still be in use.") + fail("āŒ Grill update failed. Original files might still be in use.") + if (setup.debug) { + e.printStackTrace() + } + ExitHandler.exit(1) } - } + } } } @@ -329,6 +341,12 @@ object SetupApp { ExitHandler.exit(1) } + private fun missingCompiler(): Nothing { + fail("āŒ WurstScript compiler is not installed.") + detail("Try: grill install wurstscript") + ExitHandler.exit(1) + } + private fun multipleMaps(maps: List): Nothing { log.error("āŒ Multiple maps found: ${maps.joinToString { it.fileName.toString() }}") log.info("Try: grill build ${maps.first().fileName}") @@ -1106,24 +1124,31 @@ object SetupApp { } private fun runWurstProcess(args: ArrayList, compactFallback: Boolean): WurstProcessResult { - val pb = ProcessBuilder(args) - val outputDir = compilerOutputDir() - Files.createDirectories(outputDir) - pb.directory(outputDir.toFile()) - pb.redirectErrorStream(true) - val p = pb.start() - val output = ArrayList() - p.inputStream.bufferedReader().forEachLine { line -> - output.add(line) - if (!setup.debug && isNoisyCompilerVersionLine(line)) { - return@forEachLine - } - if (!setup.quiet) { - println(line) + return try { + val pb = ProcessBuilder(args) + val outputDir = compilerOutputDir() + Files.createDirectories(outputDir) + pb.directory(outputDir.toFile()) + pb.redirectErrorStream(true) + val p = pb.start() + val output = ArrayList() + p.inputStream.bufferedReader().forEachLine { line -> + output.add(line) + if (!setup.debug && isNoisyCompilerVersionLine(line)) { + return@forEachLine + } + if (!setup.quiet) { + println(line) + } } + val exitCode = p.waitFor() + WurstProcessResult(exitCode, output) + } catch (e: Exception) { + WurstProcessResult( + 1, + listOf("Could not start Wurst compiler: ${e.message ?: e.javaClass.simpleName}") + ) } - val exitCode = p.waitFor() - return WurstProcessResult(exitCode, output) } private fun commonArgs(configData: WurstProjectConfigData): ArrayList { @@ -1193,13 +1218,16 @@ object SetupApp { return configData.withRemovedDependency(setup.commandArg) } else { log.error("āŒ Dependency is not listed in wurst.build: ${setup.commandArg}") + ExitHandler.exit(1) } return configData } private fun handleRemoveWurst() { if (!setup.requireConfirmation) { - InstallationManager.handleRemove() + if (!InstallationManager.handleRemove()) { + ExitHandler.exit(1) + } } } @@ -1259,10 +1287,18 @@ object SetupApp { val sc = Scanner(System.`in`) val line = sc.nextLine() if (line == "y") { - InstallationManager.handleUpdate() + if (!InstallationManager.handleUpdate()) { + fail("āŒ WurstScript installation failed.") + detail("Try again, or rerun with --debug for more details.") + ExitHandler.exit(1) + } } } else { - InstallationManager.handleUpdate() + if (!InstallationManager.handleUpdate()) { + fail("āŒ WurstScript installation failed.") + detail("Try again, or rerun with --debug for more details.") + ExitHandler.exit(1) + } } } else { log.info("āœ… Already up to date.") diff --git a/src/main/kotlin/global/InstallationManager.kt b/src/main/kotlin/global/InstallationManager.kt index a2fe4db..de7e9c9 100644 --- a/src/main/kotlin/global/InstallationManager.kt +++ b/src/main/kotlin/global/InstallationManager.kt @@ -80,35 +80,43 @@ object InstallationManager { } - fun handleUpdate() { + fun handleUpdate(): Boolean { val isFreshInstall = status == InstallationStatus.NOT_INSTALLED try { log.debug(if (isFreshInstall) "isInstall" else "isUpdate") log.info("ā¬ Downloading WurstScript..") - downloadCompiler(isFreshInstall) + return downloadCompiler(isFreshInstall) } catch (e: Exception) { log.error("Exception: ", e) Log.print("\n===ERROR COMPILER UPDATE===\n" + e.message + "\nPlease report here: github.com/wurstscript/WurstScript/issues\n") + return false } } - private fun downloadCompiler(isFreshInstall: Boolean) { + private fun downloadCompiler(isFreshInstall: Boolean): Boolean { + var installed = false Download.downloadCompiler { - log.info("\tšŸ“¦ Extracting..") - ZipArchiveExtractor.extractArchive(it, installDir) + log.info("\tšŸ“¦ Extracting..") + val extractionSucceeded = ZipArchiveExtractor.extractArchive(it, installDir) Files.delete(it) - val compilerJar = detectCompilerJar() - if (compilerJar == null) { - log.error("āŒ Compiler not found after extraction.") - } else { - ensureCompilerAgentDocs(compilerJar) - if (isFreshInstall) { wurstConfig = WurstConfigData() } - ensureGrillJarInstalled() - setLaunchersExecutable() - log.info("āœ” Installed WurstScript to $installDir") - } + if (!extractionSucceeded) { + log.error("āŒ Compiler archive could not be extracted.") + } else { + val compilerJar = detectCompilerJar() + if (compilerJar == null) { + log.error("āŒ Compiler not found after extraction.") + } else { + ensureCompilerAgentDocs(compilerJar) + if (isFreshInstall) { wurstConfig = WurstConfigData() } + ensureGrillJarInstalled() + setLaunchersExecutable() + log.info("āœ” Installed WurstScript to $installDir") + installed = true + } + } } + return installed } private fun setLaunchersExecutable() { @@ -146,16 +154,21 @@ object InstallationManager { return 0 } - fun handleRemove() { - val jarInUse = detectCompilerJar()?.let { !Files.isWritable(it) } ?: false - if (jarInUse) { - log.error("āŒ Cannot remove WurstScript: compiler jar is in use. Close VSCode and any running Wurst instances first.") - return - } - removeCompilerInstall() - verifyInstallation() - log.info("WurstScript has been removed.") - } + fun handleRemove(): Boolean { + val jarInUse = detectCompilerJar()?.let { !Files.isWritable(it) } ?: false + if (jarInUse) { + log.error("āŒ Cannot remove WurstScript: compiler jar is in use. Close VSCode and any running Wurst instances first.") + return false + } + removeCompilerInstall() + verifyInstallation() + if (status != InstallationStatus.NOT_INSTALLED) { + log.error("āŒ WurstScript could not be completely removed.") + return false + } + log.info("WurstScript has been removed.") + return true + } private fun removeCompilerInstall() { clearFolder(compilerDir) diff --git a/src/test/kotlin/CMDTests.kt b/src/test/kotlin/CMDTests.kt index 958bace..fdf943e 100644 --- a/src/test/kotlin/CMDTests.kt +++ b/src/test/kotlin/CMDTests.kt @@ -237,6 +237,39 @@ class CMDTests { } + @Test(priority = 10) + fun testCompilerDependentCommandsFailWhenCompilerIsMissingEvenInQuietMode() { + val project = Files.createTempDirectory("grill-missing-compiler") + Files.writeString(project.resolve("wurst.build"), "projectName: missing-compiler\n") + Files.createDirectories(project.resolve("ExampleMap.w3x")) + + val previousInstallDir = System.getProperty("wurst.install.dir") + val emptyInstallDir = Files.createTempDirectory("grill-empty-install") + try { + System.setProperty("wurst.install.dir", emptyInstallDir.toString()) + + val commands = listOf( + arrayOf(TEST, "--quiet"), + arrayOf("typecheck", "--quiet"), + arrayOf(BUILD, "ExampleMap.w3x", "--quiet"), + arrayOf("exportobjects", "ExampleMap.w3x", "--quiet") + ) + commands.forEach { args -> + val setup = SetupMain().apply { projectRoot = project } + val status = catchExit { setup.doMain(args) } + Assert.assertEquals(status, 1, "${args[0]} must fail when the compiler is missing") + } + } finally { + if (previousInstallDir == null) { + System.clearProperty("wurst.install.dir") + } else { + System.setProperty("wurst.install.dir", previousInstallDir) + } + tryDeleteRecursively(project) + tryDeleteRecursively(emptyInstallDir) + } + } + @AfterClass(alwaysRun = true) fun cleanupGeneratedProject() { tryDeleteRecursively(generatedProjectDir) From 196b16e48f06ad7600eae5c295e34ffbb6c32a6b Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 20 Aug 2026 11:24:19 +0200 Subject: [PATCH 2/2] Terminate compiler after process failures --- src/main/kotlin/file/SetupApp.kt | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/file/SetupApp.kt b/src/main/kotlin/file/SetupApp.kt index 65b6112..618ebbf 100644 --- a/src/main/kotlin/file/SetupApp.kt +++ b/src/main/kotlin/file/SetupApp.kt @@ -22,6 +22,7 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.util.* +import java.util.concurrent.TimeUnit import javax.swing.JOptionPane @@ -1124,15 +1125,17 @@ object SetupApp { } private fun runWurstProcess(args: ArrayList, compactFallback: Boolean): WurstProcessResult { + var process: Process? = null return try { val pb = ProcessBuilder(args) val outputDir = compilerOutputDir() Files.createDirectories(outputDir) pb.directory(outputDir.toFile()) pb.redirectErrorStream(true) - val p = pb.start() + val startedProcess = pb.start() + process = startedProcess val output = ArrayList() - p.inputStream.bufferedReader().forEachLine { line -> + startedProcess.inputStream.bufferedReader().forEachLine { line -> output.add(line) if (!setup.debug && isNoisyCompilerVersionLine(line)) { return@forEachLine @@ -1141,9 +1144,13 @@ object SetupApp { println(line) } } - val exitCode = p.waitFor() + val exitCode = startedProcess.waitFor() WurstProcessResult(exitCode, output) } catch (e: Exception) { + process?.let(::terminateWurstProcess) + if (e is InterruptedException) { + Thread.currentThread().interrupt() + } WurstProcessResult( 1, listOf("Could not start Wurst compiler: ${e.message ?: e.javaClass.simpleName}") @@ -1151,6 +1158,21 @@ object SetupApp { } } + private fun terminateWurstProcess(process: Process) { + if (!process.isAlive) { + return + } + process.destroy() + try { + if (process.isAlive && !process.waitFor(1, TimeUnit.SECONDS)) { + process.destroyForcibly() + } + } catch (_: InterruptedException) { + process.destroyForcibly() + Thread.currentThread().interrupt() + } + } + private fun commonArgs(configData: WurstProjectConfigData): ArrayList { val args = ArrayList(InstallationManager.compilerLaunchCommand().toList())