Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin coroutine for executing external process

The traditional approach in Java to execute an external process is to start a new Process, start two threads to consume its inputStream and errorStream and then call its blocking Process.waitFor() to wait till the external command has exited.

How can this be done in a (almost) non-blocking style with Kotlin coroutines?

I tried it this way. Do you have any suggestions to improve it?

(How to asynchronously read the streams, also call ProcessBuilder.start() in withContext(Dispatchers.IO), are there too many calls to Dispatchers.IO, ...?)

    suspend fun executeCommand(commandArgs: List<String>): ExecuteCommandResult {
        try {
            val process = ProcessBuilder(commandArgs).start()

            val outputStream = GlobalScope.async(Dispatchers.IO) { readStream(process.inputStream) }
            val errorStream = GlobalScope.async(Dispatchers.IO) { readStream(process.errorStream) }

            val exitCode = withContext(Dispatchers.IO) {
                process.waitFor()
            }

            return ExecuteCommandResult(exitCode, outputStream.await(), errorStream.await())
        } catch (e: Exception) {
            return ExecuteCommandResult(-1, "", e.localizedMessage)
        }
    }

    private suspend fun readStream(inputStream: InputStream): String {
        val readLines = mutableListOf<String>()

        withContext(Dispatchers.IO) {
            try {
                inputStream.bufferedReader().use { reader ->
                    var line: String?

                    do {
                        line = reader.readLine()

                        if (line != null) {
                            readLines.add(line)
                        }
                    } while (line != null)
                }
            } catch (e: Exception) {
                // ..
            }
        }

        return readLines.joinToString(System.lineSeparator())
    }
like image 432
dankito Avatar asked Feb 28 '26 06:02

dankito


1 Answers

I just bumped into this 3 year old question and thought about making improvements to this code. I think that this code does already the most optimized approach because we can't really do anything when it comes to blocking operations from the source, but I think you can save some context switching and improve the code like this:

suspend fun executeCommand(commandArgs: List<String>): ProcessResult = withContext(
    Dispatchers.IO
) {
    runCatching {
        val process = ProcessBuilder(commandArgs).start()
        val outputStream = async {
            println("Context for output stream -> $coroutineContext -> Thread -> ${Thread.currentThread()}")
            readStream(process.inputStream) }
        val errorStream = async {
            println("Context for error stream -> $coroutineContext -> Thread -> ${Thread.currentThread()}")
            readStream(process.errorStream)
        }
        println("Context for exit code -> $coroutineContext -> Thread -> ${Thread.currentThread()}")
        val exitCode =  process.waitFor()
        ProcessResult(
            exitCode = exitCode,
            message = outputStream.await(),
            errorMessage = errorStream.await()
        )
    }.onFailure{
        ProcessResult(
            exitCode = -1,
            message = "",
            errorMessage = it.localizedMessage
        )
    }.getOrThrow()
}

private fun readStream(inputStream: InputStream) =
    inputStream.bufferedReader().use { reader -> reader.readText() }

data class ProcessResult(val exitCode: Int, val message: String, val errorMessage: String)

Just ignore the prinln. I leave it there if you want to see it clearly on which context the coroutines are running. It makes it also more visible to see that they share the same scope and they run on different threads. But they should be removed when going to production code.

like image 96
Joao Esperancinha Avatar answered Mar 03 '26 00:03

Joao Esperancinha



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!