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?

artem
  • 16,382
  • 34
  • 113
  • 189
  • Does the command produce any output to the terminal when you run it manually? – Joni Mar 27 '20 at 20:02
  • @Joni yep, it prints lots of text to the stdout – artem Mar 27 '20 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 – Joni Mar 27 '20 at 23:58
  • @Joni so, how to fix it? – artem Mar 28 '20 at 00: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. – Joni Mar 28 '20 at 01:28
  • @Joni yes, I need the output of this command. – artem Mar 28 '20 at 01:40

1 Answers1

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.

Andreas
  • 154,647
  • 11
  • 152
  • 247