0

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?

asked Mar 27, 2020 at 19:50
6
  • Does the command produce any output to the terminal when you run it manually? Commented Mar 27, 2020 at 20:02
  • @Joni yep, it prints lots of text to the stdout Commented Mar 27, 2020 at 23:19
  • 1
    Well that's your problem then. You're waiting for the process to finish before reading the output. While you wait, the buffer for the output fills up. The process stalls trying to write more output, and never finishes... What Andreas says in his answer Commented Mar 27, 2020 at 23:58
  • @Joni so, how to fix it? Commented Mar 28, 2020 at 0:03
  • Do you need to collect the output? If not, use Redirect.INHERIT instead of PIPE. If you do, things get complicated. Maybe consider sending the output to files as a first step. Commented Mar 28, 2020 at 1:28

1 Answer 1

4

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.

answered Mar 27, 2020 at 20:08
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.