Skip to content

05nelsonm/kmp-process

Folders and files

NameName
Last commit message
Last commit date

Latest commit

eb564c9 · Feb 26, 2025

History

97 Commits
Feb 16, 2025
Feb 26, 2025
Feb 26, 2025
Feb 13, 2025
Feb 26, 2025
Feb 16, 2025
Feb 26, 2025
Feb 1, 2024
Feb 13, 2025
Feb 26, 2025
Feb 1, 2024
Feb 26, 2025
Dec 15, 2024
Feb 16, 2025
Feb 26, 2025
Feb 1, 2024
Feb 1, 2024
Feb 26, 2025

Repository files navigation

kmp-process

badge-license badge-latest-release

badge-kotlin badge-coroutines badge-bitops 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 docs available at https://kmp-process.matthewnelson.io

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

NOTE: Spawning of processes for Apple mobile targets will work on simulators when utilizing executables compiled for macOS. Unfortunately due to the com.apple.security.app-sandbox entitlement inhibiting modification of a file's permissions to set as executable, posix_spawn will likely fail on the device (unless executing a file already accessible on the OS that is executable).

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
    // (extension available for non-apple mobile).
    .changeDir(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.2.0")
}