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