Refactor ProcessRunner to simplify command execution and improve process termination handling

This commit is contained in:
DerDavidBohl
2025-12-10 13:40:52 +01:00
parent c657e906f2
commit f82d2046fa
2 changed files with 15 additions and 44 deletions
+5 -3
View File
@@ -18,11 +18,13 @@ RUN mvn clean package -DskipTests
# Use OpenJDK image to run the application
FROM eclipse-temurin:25-alpine
# Install Docker and git
RUN apk add docker docker-compose git
# Install Docker, git, and tini (init system to reap zombies)
RUN apk add docker docker-compose git tini
# Finish
WORKDIR /app
COPY --from=backend-build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar", "--spring.profiles.active=production"]
# Use tini as init to reap zombie processes
ENTRYPOINT ["/sbin/tini", "--", "java", "-jar", "app.jar", "--spring.profiles.active=production"]
@@ -52,24 +52,7 @@ public class ProcessRunner {
finalEnv.putAll(env);
}
// Critical: Use shell to run commands so we can kill the entire process group
// This prevents git's child processes from becoming zombies
String osName = System.getProperty("os.name").toLowerCase();
boolean isWindows = osName.contains("win");
List<String> finalCommand;
if (isWindows) {
// Windows: use cmd /c
finalCommand = new java.util.ArrayList<>();
finalCommand.add("cmd");
finalCommand.add("/c");
finalCommand.addAll(commandParts);
} else {
// Linux/Unix: use sh -c with process group
finalCommand = List.of("sh", "-c", String.join(" ", commandParts));
}
ProcessBuilder processBuilder = new ProcessBuilder(finalCommand);
ProcessBuilder processBuilder = new ProcessBuilder(commandParts);
processBuilder.directory(workingDirectory);
processBuilder.environment().putAll(finalEnv);
@@ -93,7 +76,8 @@ public class ProcessRunner {
if (!finished) {
log.warn("Process timed out: {}", String.join(" ", commandParts));
killProcess(process);
process.destroyForcibly();
process.waitFor(5, TimeUnit.SECONDS);
}
// Wait for output streams to finish reading
@@ -105,17 +89,17 @@ public class ProcessRunner {
} catch (InterruptedException e) {
log.warn("Process interrupted: {}", String.join(" ", commandParts), e);
if (process != null && process.isAlive()) {
killProcess(process);
process.destroyForcibly();
try {
process.waitFor(5, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
Thread.currentThread().interrupt();
} finally {
// Ensure process is terminated and streams are closed
// Close streams to release resources
if (process != null) {
if (process.isAlive()) {
log.warn("Force killing remaining process");
killProcess(process);
}
// Close all streams to release resources and prevent leaks
closeQuietly(process.getInputStream());
closeQuietly(process.getOutputStream());
closeQuietly(process.getErrorStream());
@@ -136,21 +120,6 @@ public class ProcessRunner {
}
}
/**
* Kills process and waits for it to die (reaps zombie)
*/
private void killProcess(Process process) {
log.debug("Killing process: {} (alive: {})", process.pid(), process.isAlive());
process.destroyForcibly();
// CRITICAL: Must waitFor() to reap the zombie
try {
process.waitFor(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private Thread readStream(java.io.InputStream inputStream, StringBuilder output) {
Thread reader = new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {