Cancelling a build only killed the running build process; the synchronous preparation phases — most notably a multi-minute Docker image build, but also the Gradle-volume preparation — ran to completion and kept the concurrency slot occupied, so the next queued build stayed PENDING for a long time. Build runners now report every auxiliary process through an onAuxProcess sink (GitCommandRunner gained an onProcess hook), and the executor registers them like the build process, so cancellation terminates whatever is currently running. Measured on vm4006: cancel to next-build-running is ~3s in the normal case; the unit test covers the aux-phase case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65 lines
2.1 KiB
Kotlin
65 lines
2.1 KiB
Kotlin
package de.hoennig.gittally.git
|
|
|
|
import org.springframework.stereotype.Component
|
|
import java.nio.file.Path
|
|
import java.util.concurrent.CompletableFuture
|
|
|
|
data class GitCommandResult(
|
|
val exitCode: Int,
|
|
val stdout: String,
|
|
val stderr: String,
|
|
) {
|
|
val isSuccess: Boolean get() = exitCode == 0
|
|
|
|
fun lines(): List<String> = stdout.lines().map { it.trim() }.filter { it.isNotEmpty() }
|
|
}
|
|
|
|
class GitCommandException(
|
|
command: List<String>,
|
|
result: GitCommandResult,
|
|
) : RuntimeException(
|
|
"Command failed with exit code ${result.exitCode}: ${command.joinToString(" ")}\n${result.stderr.trim()}",
|
|
)
|
|
|
|
@Component
|
|
class GitCommandRunner {
|
|
/** [onProcess] receives the started process, so callers can terminate it early (build cancellation). */
|
|
fun run(
|
|
command: List<String>,
|
|
workingDir: Path,
|
|
environment: Map<String, String> = emptyMap(),
|
|
onProcess: (Process) -> Unit = {},
|
|
): GitCommandResult {
|
|
val builder = ProcessBuilder(command).directory(workingDir.toFile())
|
|
builder.environment().putAll(environment)
|
|
val process = builder.start()
|
|
onProcess(process)
|
|
// stderr is drained concurrently so neither pipe buffer can block the process;
|
|
// reads tolerate closed streams, e.g. when onProcess terminated the process early
|
|
val stderr = CompletableFuture.supplyAsync { process.errorStream.readSafely() }
|
|
val stdout = process.inputStream.readSafely()
|
|
val exitCode = process.waitFor()
|
|
return GitCommandResult(exitCode, stdout, stderr.get())
|
|
}
|
|
|
|
private fun java.io.InputStream.readSafely(): String =
|
|
try {
|
|
bufferedReader().readText()
|
|
} catch (_: java.io.IOException) {
|
|
""
|
|
}
|
|
|
|
fun runOrThrow(
|
|
command: List<String>,
|
|
workingDir: Path,
|
|
environment: Map<String, String> = emptyMap(),
|
|
onProcess: (Process) -> Unit = {},
|
|
): GitCommandResult {
|
|
val result = run(command, workingDir, environment, onProcess)
|
|
if (!result.isSuccess) {
|
|
throw GitCommandException(command, result)
|
|
}
|
|
return result
|
|
}
|
|
}
|