OverviewFilesCommitsIssuesDocsReleasesPeers

celltron-node / app / src / main / java / net / celltron / node / ProotLauncher.kt

105 lines · kt ref:
package net.celltron.node

import android.util.Log

/**
 * Builds and runs the proot invocation that boots the pgh stack inside the glibc
 * rootfs, and supervises it.
 *
 * The proot process is a DIRECT CHILD of the host service process — never
 * detached (no setsid). That anchoring is deliberate and load-bearing: on
 * Android a detached process tree gets reaped (the celltron no-setsid lesson —
 * a `setsid supervise.sh &` had the OS reap the whole tree in ~18 min while the
 * launcher survived). Keeping proot parented to the foreground service is what
 * makes the tree live exactly as long as the service.
 *
 * Mirrors the proven celltron.sh launcher:
 *   proot -0 -r <rootfs> -b /dev -b /proc -b /sys -b <tmp>:/tmp -w /root \
 *     /usr/bin/env -i HOME=/root ... PATH=... <guest command>
 * with PROOT_TMP_DIR set on proot's OWN environment (proot reads it there, not
 * from the guest `env -i` set).
 */
class ProotLauncher(private val layout: Layout) {

    @Volatile private var proc: Process? = null
    @Volatile private var stopped = false

    /**
     * Step-1 harness smoke test: run a trivial guest command to prove the
     * extract -> proot -> env -> supervise loop works on-device, decoupled from
     * the full sidecar rootfs. Flip to false once the full rootfs.tar (with the
     * pgh-sidecar bundle baked in) is staged.
     */
    private val smokeTest = true

    /**
     * Guest command to run under proot. Smoke: print uname + a marker, then hold
     * (like a daemon would) so the supervisor doesn't hot-loop. Real: boot the
     * pgh-sidecar on an abstract socket (pathname UDS is SELinux-denied no-root;
     * @-names go through bare-abstract-socket, baked into the sidecar bundle).
     */
    private val guestCmd: List<String> = if (smokeTest) {
        listOf("sh", "-c", "uname -a; echo celltron-node-smoke-ok; sleep 86400")
    } else {
        listOf(
            "/root/pgh-sidecar-bootstrap-bare/runtime/bare",
            "/root/pgh-sidecar-bootstrap-bare/bin/pgh-sidecar.bare.js"
        )
    }

    fun command(): List<String> = buildList {
        add(layout.proot.absolutePath)
        add("-0")                                    // fake root
        add("-r"); add(layout.rootfs.absolutePath)   // new root
        add("-b"); add("/dev")
        add("-b"); add("/proc")
        add("-b"); add("/sys")
        add("-b"); add("${layout.tmp.absolutePath}:/tmp")
        add("-w"); add("/root")
        add("/usr/bin/env"); add("-i")               // clean guest env
        add("HOME=/root")
        add("TERM=xterm")
        add("LANG=C.UTF-8")
        add("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
        add("PGH_SIDECAR_SOCK=@pgh-node")            // abstract UDS (SELinux-safe)
        addAll(guestCmd)
    }

    /** Blocking supervise loop — call on a background thread. Restarts on exit. */
    fun superviseForever(onLine: (String) -> Unit) {
        var backoff = 1000L
        while (!stopped) {
            val startedAt = System.currentTimeMillis()
            try {
                val pb = ProcessBuilder(command()).redirectErrorStream(true)
                // proot reads PROOT_TMP_DIR from its own env (Android has no /tmp).
                pb.environment()["PROOT_TMP_DIR"] = layout.tmp.absolutePath
                Log.i(Extractor.TAG, "launch: ${command().joinToString(" ")}")
                val p = pb.start().also { proc = it }
                p.inputStream.bufferedReader().forEachLine { line ->
                    Log.i(Extractor.TAG, line)
                    onLine(line)
                }
                val rc = p.waitFor()
                Log.w(Extractor.TAG, "guest exited rc=$rc")
            } catch (e: Exception) {
                Log.e(Extractor.TAG, "supervise error: ${e.message}")
            }
            if (stopped) break
            // Reset backoff after a healthy run (>60s); otherwise grow it so a
            // crash-on-startup doesn't hot-loop.
            backoff = if (System.currentTimeMillis() - startedAt > 60_000L) {
                1000L
            } else {
                (backoff * 2).coerceAtMost(30_000L)
            }
            try { Thread.sleep(backoff) } catch (e: InterruptedException) { break }
        }
    }

    fun stop() {
        stopped = true
        try { proc?.destroy() } catch (e: Exception) {}
    }
}