|
| 1 | +import java.io.Reader |
| 2 | +import java.time.Duration |
| 3 | +import java.util.concurrent.TimeUnit |
| 4 | +import kotlin.concurrent.thread |
| 5 | + |
| 6 | +object ProcessUtil { |
| 7 | + data class Result( |
| 8 | + val exitCode: Int, |
| 9 | + val stdout: String, |
| 10 | + val stderr: String, |
| 11 | + val isTimeout: Boolean, // true if the process was terminated due to timeout |
| 12 | + ) |
| 13 | + |
| 14 | + fun run( |
| 15 | + command: List<String>, |
| 16 | + input: String? = null, |
| 17 | + timeout: Duration? = null, |
| 18 | + builder: ProcessBuilder.() -> Unit = {}, |
| 19 | + ): Result { |
| 20 | + val reader = input?.reader() ?: "".reader() |
| 21 | + return run(command, reader, timeout, builder) |
| 22 | + } |
| 23 | + |
| 24 | + fun run( |
| 25 | + command: List<String>, |
| 26 | + input: Reader, |
| 27 | + timeout: Duration? = null, |
| 28 | + builder: ProcessBuilder.() -> Unit = {}, |
| 29 | + ): Result { |
| 30 | + val process = ProcessBuilder(command).apply(builder).start() |
| 31 | + return communicate(process, input, timeout) |
| 32 | + } |
| 33 | + |
| 34 | + private fun communicate( |
| 35 | + process: Process, |
| 36 | + input: Reader, |
| 37 | + timeout: Duration?, |
| 38 | + ): Result { |
| 39 | + // Handle process input (stdin) |
| 40 | + val stdinThread = thread { |
| 41 | + process.outputStream.bufferedWriter().use { writer -> |
| 42 | + input.copyTo(writer) |
| 43 | + writer.flush() |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + // Capture process output (stdout) |
| 48 | + val stdout = StringBuilder() |
| 49 | + val stdoutThread = thread { |
| 50 | + process.inputStream.bufferedReader().useLines { lines -> |
| 51 | + lines.forEach { stdout.appendLine(it) } |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + // Capture process error output (stderr) |
| 56 | + val stderr = StringBuilder() |
| 57 | + val stderrThread = thread { |
| 58 | + process.errorStream.bufferedReader().useLines { lines -> |
| 59 | + lines.forEach { stderr.appendLine(it) } |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + // Start all threads |
| 64 | + stdinThread.start() |
| 65 | + stdoutThread.start() |
| 66 | + stderrThread.start() |
| 67 | + |
| 68 | + // Wait for completion |
| 69 | + val isTimeout = if (timeout != null) { |
| 70 | + !process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS) |
| 71 | + } else { |
| 72 | + process.waitFor() |
| 73 | + false |
| 74 | + } |
| 75 | + |
| 76 | + // If timeout occurred, destroy the process forcibly |
| 77 | + if (isTimeout) { |
| 78 | + process.destroyForcibly() |
| 79 | + } |
| 80 | + |
| 81 | + // Wait for stream threads to finish |
| 82 | + stdinThread.join() |
| 83 | + stdoutThread.join() |
| 84 | + stderrThread.join() |
| 85 | + |
| 86 | + return Result( |
| 87 | + exitCode = if (isTimeout) -1 else process.exitValue(), |
| 88 | + stdout = stdout.toString(), |
| 89 | + stderr = stderr.toString(), |
| 90 | + isTimeout = isTimeout |
| 91 | + ) |
| 92 | + } |
| 93 | +} |
0 commit comments