import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.time.*; import java.util.*; import java.util.concurrent.*; /** Executes argument lists directly, without shell interpolation or credential logging. */ class Commands { record Result(int code, String output) { String checked() { if (code != 0) throw new Failure(code, "Command failed with exit status " + code); return output.strip(); } } static class Failure extends RuntimeException { final int exitCode; Failure(int exitCode, String message) { super(message); this.exitCode = exitCode; } } Instant deadline; volatile Process active; Path log; void renew(Duration timeout) { deadline = Instant.now().plus(timeout); } void cancel() { Process process = active; if (process != null) stop(process); } Commands(Duration timeout) { deadline = Instant.now().plus(timeout); } Result execute(List args, Path cwd, Map env, String input, boolean echo) throws Exception { if (!Instant.now().isBefore(deadline)) throw new Failure(124, "CI time limit exceeded"); ProcessBuilder builder = new ProcessBuilder(args).directory(cwd.toFile()).redirectErrorStream(true); builder.environment().clear(); builder.environment().putAll(env); Process process = builder.start(); active = process; ByteArrayOutputStream captured = new ByteArrayOutputStream(); Path destination = log; try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { Future reader = executor.submit(() -> { try (InputStream stream = process.getInputStream(); OutputStream file = destination == null ? OutputStream.nullOutputStream() : Files.newOutputStream(destination, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { byte[] buffer = new byte[8192]; int count; while ((count = stream.read(buffer)) != -1) { captured.write(buffer, 0, count); if (echo) { System.out.write(buffer, 0, count); System.out.flush(); } file.write(buffer, 0, count); } } catch (IOException e) { throw new UncheckedIOException(e); } }); try { try (OutputStream stdin = process.getOutputStream()) { if (input != null) stdin.write(input.getBytes(StandardCharsets.UTF_8)); } if (!process.waitFor(Math.max(1, Duration.between(Instant.now(), deadline).toMillis()), TimeUnit.MILLISECONDS)) { stop(process); throw new Failure(124, "CI time limit exceeded"); } reader.get(10, TimeUnit.SECONDS); return new Result(process.exitValue(), captured.toString(StandardCharsets.UTF_8)); } finally { if (process.isAlive()) stop(process); active = null; } } } private static void stop(Process process) { process.descendants().forEach(ProcessHandle::destroyForcibly); process.destroyForcibly(); } }