0

I am translating code from a NodeJS server to a Kotlin Ktor server.

The NodeJS code splits the output between a String to be handled by code, and realtime server logging :

const shellScript = exec("./myScript.sh",
  (error, stdout, stderr) => {
   // this happens when the script ends
   // stdout and stderr contain full script output
    if (error === null) {
      ...
    }
  });

// Realtime server logging
shellScript.stdout.on('data', (data) => { console.log(data); });
shellScript.stderr.on('data', (data) => { console.error(data); });

According to my understanding of java.lang.ProcessBuilder, we would need to spawn a thread (or a coroutine) to poll inputStream and errorStream in a loop and accumulate them in a volatile field.

Is there a cleaner way to do this?

JM Lord
  • 1,031
  • 1
  • 13
  • 29
  • This may be of help: https://stackoverflow.com/questions/35421699/how-to-invoke-external-command-from-within-kotlin-code/41495542 – David Soroko Jan 11 '22 at 08:23
  • Thanks, unfortunately I had already checked that thread. According to my understanding and tests, all these answers give the logs only at the process end, which is not very good for long-running tasks. – JM Lord Jan 11 '22 at 16:03
  • Oh, OK. Do you know if your long running tasks flush regularly? – David Soroko Jan 11 '22 at 16:36
  • Yes they do. By the way, I have implemented the above with ProcessBuilder and a coroutine; much more bulky than the original JavaScript code, but at least it works. I'll post it as an answer if nothing cleaner comes up in the next few days... – JM Lord Jan 11 '22 at 18:31

1 Answers1

1

Here is the ProcessBuilder solution, which I initially wanted to avoid. It does the job though it is bulky. Let me know if a better API is made available!

var logs:String = ""
runCatching {
    var command:List<String> = listOf("command", "arg")
    parameters.params?.let {command += it} // dynamic args
    ProcessBuilder(command)
        .directory(File(scriptRoot))
        .redirectOutput(ProcessBuilder.Redirect.PIPE)
        .redirectErrorStream(true) // Merges stderr into stdout
        .start().also { process -> 
            withContext(Dispatchers.IO) { // More info on this context switching : https://elizarov.medium.com/blocking-threads-suspending-coroutines-d33e11bf4761
                launch {
                    process.inputStream.bufferedReader().run {
                        while (true) { // Breaks when readLine returns null
                            readLine()?.let { line ->
                                logger.trace(line) // realtime logging
                                logs += "$line\n" // record
                            } ?: break
                        }
                    }
                }
    
                process.waitFor(60, TimeUnit.MINUTES)
                if(process.isAlive) {
                    logs += "TIMEOUT occurred".also { logger.warn(it) } + "\n"
                    process.destroy()
                }
            }
        }
}.onSuccess { process ->
    if(process.exitValue() == 0) {
        // completed with success
    } else {
        // completed with failure
    }
    
}.onFailure { ex ->
    logs = ex.stackTraceToString()
}

// Logs are available in $logs
JM Lord
  • 1,031
  • 1
  • 13
  • 29