05nelsonm / kmp-process

Process implementation for Kotlin Multiplatform
Apache License 2.0
30 stars 1 forks source link

kmp-process

badge-license badge-latest-release

badge-kotlin badge-coroutines badge-endians badge-immutable badge-kmp-file

badge-platform-android badge-platform-jvm badge-platform-js-node badge-platform-linux badge-platform-ios badge-platform-macos badge-support-apple-silicon badge-support-linux-arm

Process implementation for Kotlin Multiplatform.

API is highly inspired by Node.js child_process and Rust Command

Info

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+.

Example

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()
}

Get Started

dependencies {
    implementation("io.matthewnelson.kmp-process:process:0.1.0-rc01")
}