I run a command with this code:
open class AppRunner {
fun run(
app: String,
args: Array<String>,
timeoutAmount: Long = 6000,
timeoutUnit: TimeUnit = TimeUnit.SECONDS
): AppResult {
val command = mutableListOf(app)
.apply {
addAll(args)
}
val commandString = command.joinToString(" ") { "\"$it\"" }
Kimber.d("Executing command: $commandString")
val processResult = ProcessBuilder(command)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
.apply {
waitFor(timeoutAmount, timeoutUnit)
}
val exitCode = processResult.exitValue()
val stdOut = processResult.inputStream.bufferedReader().readText()
val stdErr = processResult.errorStream.bufferedReader().readText()
return AppResult(exitCode, stdOut, stdErr)
}
data class AppResult(
val exitCode: Int,
val stdOut: String,
val stdErr: String
) {
fun isSuccessful(): Boolean = exitCode == 0
fun getStdOutLines(): List<String> = stdOut.split("\n")
fun getStdErrLines(): List<String> = stdOut.split("\n")
}
}
like this:
val args = arrayOf(
audioFile.absolutePath,
"-r",
getRecognizer(language),
"-f",
"json",
"-q"
)
val result = appRunner.run(rhubarbBinary.absolutePath, args)
For some programs like ffmpeg
it works, but the example above does not.
«Raw» command is "/Users/user/<path>/rhubarb" "/var/folders/g6/bmyctvjn7fl3m8kdr0cs1hk80000gn/T/lipsync_audio_14979831388784829375.wav" "-r" "phonetic" "-f" "json" "-q"
, if I run it manually, it works fine.
But if I run it with the code above, it just does not launch and freezes.
I'm sure that it's not launched because this command takes about 30 seconds to complete and consumes 100% CPU while running, and when running it with this code it does not load CPU at all.
I use Kotlin 1.3.71 on JVM 8, macOS 10.15.4.
What's wrong?
You wait for the program to end before you being reading the piped output, but a pipe only has a limited buffer, so when the buffer is full, the program will wait for you consume the buffered output, but you're waiting on the program to end. Deadlock!
Always consume output before calling waitFor()
.
UPDATE
Recommend you change the code as follows:
val process = ProcessBuilder(command)
.redirectErrorStream(true)
.start()
val stdOut = processResult.inputStream.bufferedReader().readText()
if (process.waitFor(timeoutAmount, timeoutUnit)) {
val exitCode = processResult.exitValue()
return AppResult(exitCode, stdOut, "")
}
// timeout: decide what to do here, since command hasn't terminated yet
There is no need to specify Redirect.PIPE
, since that's the default. If you don't join stderr and stdout like shown here, you'd need to create threads to consume them individually, since they both have the buffer full issue, so you can't just read one of them first.