Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/main/kotlin/config/WurstProjectConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -230,4 +231,3 @@ object WurstProjectConfig {
" },\n" +
"\t\"search.useIgnoreFiles\": false }"
}

118 changes: 88 additions & 30 deletions src/main/kotlin/file/SetupApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -231,18 +232,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 -> {
Expand All @@ -266,11 +271,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)
}
}
}
Expand All @@ -288,7 +295,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)
}
}
Expand All @@ -300,9 +309,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)
}
}
}
}

}
Expand All @@ -329,6 +342,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<Path>): Nothing {
log.error("❌ Multiple maps found: ${maps.joinToString { it.fileName.toString() }}")
log.info("Try: grill build ${maps.first().fileName}")
Expand Down Expand Up @@ -1106,24 +1125,52 @@ object SetupApp {
}

private fun runWurstProcess(args: ArrayList<String>, 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<String>()
p.inputStream.bufferedReader().forEachLine { line ->
output.add(line)
if (!setup.debug && isNoisyCompilerVersionLine(line)) {
return@forEachLine
var process: Process? = null
return try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Terminate the compiler process when handling it throws

If reading the compiler output or waitFor() throws after pb.start() succeeds—for example, when the calling thread is interrupted—this broad catch returns a failure result without waiting for or terminating the child. The caller then exits Grill with status 1 while the orphaned compiler may continue modifying map or build outputs; retain the Process and destroy it on exceptional paths, or limit this catch to failures that occur before the process starts.

Useful? React with 👍 / 👎.

val pb = ProcessBuilder(args)
val outputDir = compilerOutputDir()
Files.createDirectories(outputDir)
pb.directory(outputDir.toFile())
pb.redirectErrorStream(true)
val startedProcess = pb.start()
process = startedProcess
val output = ArrayList<String>()
startedProcess.inputStream.bufferedReader().forEachLine { line ->
output.add(line)
if (!setup.debug && isNoisyCompilerVersionLine(line)) {
return@forEachLine
}
if (!setup.quiet) {
println(line)
}
}
if (!setup.quiet) {
println(line)
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}")
)
}
}

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()
}
val exitCode = p.waitFor()
return WurstProcessResult(exitCode, output)
}

private fun commonArgs(configData: WurstProjectConfigData): ArrayList<String> {
Expand Down Expand Up @@ -1193,13 +1240,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)
}
}
}

Expand Down Expand Up @@ -1259,10 +1309,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.")
Expand Down
63 changes: 38 additions & 25 deletions src/main/kotlin/global/InstallationManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions src/test/kotlin/CMDTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading