Process
implementation for Kotlin Multiplatform.
API is highly inspired by Node.js
child_process
and Rust
Command
Process Creation Method Used | |
---|---|
Android |
java.lang.ProcessBuilder |
Jvm |
java.lang.ProcessBuilder |
Node.js |
spawn and spawnSync |
Linux |
posix_spawn or fork/execve |
macOS |
posix_spawn or fork/execve |
iOS |
posix_spawn |
NOTE: java.lang.ProcessBuilder
and java.lang.Process
Java 8
functionality is backported for Android and tested against API 15+.
NOTE: Async API usage on Jvm
& Android
requires the kotlinx.coroutines.core
dependency.
val builder = Process.Builder(command = "cat")
// Optional arguments
.args("--show-ends")
// Also accepts vararg and List<String>
.args("--number", "--squeeze-blank")
// Change the process's working
// directory.
//
// WARNING: iOS is the only platform
// that this functionality is not supported
// on. Declaring chdir on iOS will result
// in a failure to spawn the process.
.chdir(myApplicationDir)
// Modify the Signal to send the Process
// when Process.destroy is called (only sent
// if the Process has not completed yet).
.destroySignal(Signal.SIGKILL)
// Take input from a file
.stdin(Stdio.File.of("build.gradle.kts"))
// Pipe output to system out
.stdout(Stdio.Inherit)
// Dump error output to log file
.stderr(Stdio.File.of("logs/example_cat.err"))
// Modify the environment variables inherited
// from the current process (parent).
.environment {
remove("HOME")
// ...
}
// shortcut to set/overwrite an environment
// variable
.environment("HOME", myApplicationDir.path)
// Spawned process (Blocking APIs for Jvm/Native)
builder.spawn().let { p ->
try {
val exitCode: Int? = p.waitFor(250.milliseconds)
if (exitCode == null) {
println("Process did not complete after 250ms")
// do something
}
} finally {
p.destroy()
}
}
// Spawned process (Async APIs for all platforms)
myScope.launch {
// Use spawn {} (with lambda) which will
// automatically call destroy upon lambda closure,
// instead of needing the try/finally block.
builder.spawn { p ->
val exitCode: Int? = p.waitForAsync(500.milliseconds)
if (exitCode == null) {
println("Process did not complete after 500ms")
// do something
}
// wait until process completes. If myScope
// is cancelled, will automatically pop out.
p.waitForAsync()
} // << Process.destroy automatically called on closure
}
// Direct output (Blocking API for all platforms)
builder.output {
maxBuffer = 1024 * 24
timeoutMillis = 500
}.let { output ->
println(output.stdout)
println(output.stderr)
println(output.processError ?: "no errors")
println(output.processInfo)
}
// Piping output (feeds are only functional with Stdio.Pipe)
builder.stdout(Stdio.Pipe).stderr(Stdio.Pipe).spawn { p ->
val exitCode = p.stdoutFeed { line ->
// single feed lambda
// line dispatched from `stdout` bg thread (Jvm/Native)
println(line)
}.stderrFeed(
// vararg for attaching multiple OutputFeed at once
// so no data is missed (reading starts on the first
// OutputFeed attachment for that Pipe)
OutputFeed { line ->
// line dispatched from `stderr` bg thread (Jvm/Native)
println(line)
},
OutputFeed { line ->
// do something else
},
).waitFor(5.seconds)
println("EXIT_CODE[$exitCode]")
} // << Process.destroy automatically called on closure
// Wait for asynchronous stdout/stderr output to stop
// after Process.destroy is called
myScope.launch {
val exitCode = builder.spawn { p ->
p.stdoutFeed { line ->
// do something
}.stderrFeed { line ->
// do something
}.waitForAsync(50.milliseconds)
p // return Process to spawn lambda
} // << Process.destroy automatically called on closure
// blocking APIs also available for Jvm/Native
.stdoutWaiter()
.awaitStopAsync()
.stderrWaiter()
.awaitStopAsync()
.waitForAsync()
println("EXIT_CODE[$exitCode]")
}
// Error handling API for "internal-ish" process errors.
// By default, ProcessException.Handler.IGNORE is used,
// but you may supplement that with your own handler.
builder.onError { e ->
// e is always an instance of ProcessException
//
// Throwing an exception from here will be caught,
// the process destroyed (to prevent zombie processes),
// and then be re-thrown. That will likely cause a crash,
// but you can do it and know that the process has been
// cleaned up before getting crazy.
when (e.context) {
ProcessException.CTX_DESTROY -> {
// Process.destroy had an issue, such as a
// file descriptor closure failure on Native.
e.cause.printStackTrace()
}
ProcessException.CTX_FEED_STDOUT,
ProcessException.CTX_FEED_STDERR -> {
// An attached OutputFeed threw exception
// when a line was dispatched to it. Let's
// get crazy and potentially crash the app.
throw e
}
// Currently, the only other place a ProcessException
// will come from is the `Node.js` implementation's
// ChildProcess error listener.
else -> e.printStackTrace()
}
}.spawn { p ->
p.stdoutFeed { line ->
myOtherClassThatHasABugAndWillThrowException.parse(line)
}.waitFor()
}
dependencies {
implementation("io.matthewnelson.kmp-process:process:0.1.0")
}