package net.celltron.node
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
/**
* Foreground service = the anchor for the whole proot/pgh process tree.
*
* It holds a partial WakeLock (the app-level replacement for Termux's wake-lock
* plus the stay_on hack) so Doze doesn't suspend the node, extracts the runtime
* host on first launch, then supervises the pgh stack. Because the proot child
* is parented to this service and never setsid'd, it lives as long as the
* service — the fix for the reaping that killed the Termux setup.
*
* Step 1 scope: bring the runtime host up and boot pgh-sidecar on @pgh-node.
* The GUI bridge (WebView + LocalSocket ABSTRACT client) lands in a later step.
*/
class PghNodeService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var launcher: ProotLauncher? = null
private var worker: Thread? = null
companion object {
/** Last log line from the supervised guest, for the launcher UI to poll. */
@Volatile
var lastLogSnapshot: String = "starting…"
}
override fun onCreate() {
super.onCreate()
startForegroundCompat()
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "celltron-node:wl").apply {
setReferenceCounted(false)
acquire()
}
Log.i(Extractor.TAG, "wakelock held=${wakeLock?.isHeld}")
worker = Thread {
try {
val layout = Extractor.ensure(applicationContext)
launcher = ProotLauncher(layout)
launcher!!.superviseForever { line -> lastLogSnapshot = line }
} catch (e: Exception) {
Log.e(Extractor.TAG, "node failed: ${e.message}", e)
lastLogSnapshot = "ERROR: ${e.message}"
}
}.also {
it.isDaemon = true
it.name = "celltron-node-supervisor"
it.start()
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
launcher?.stop()
wakeLock?.let { if (it.isHeld) it.release() }
super.onDestroy()
}
private fun startForegroundCompat() {
val chId = "celltron-node"
if (Build.VERSION.SDK_INT >= 26) {
val ch = NotificationChannel(chId, "celltron node", NotificationManager.IMPORTANCE_LOW)
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
.createNotificationChannel(ch)
}
val builder = if (Build.VERSION.SDK_INT >= 26) {
Notification.Builder(this, chId)
} else {
@Suppress("DEPRECATION")
Notification.Builder(this)
}
val notif = builder
.setContentTitle("celltron node")
.setContentText("pgh stack running")
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setOngoing(true)
.build()
startForeground(1, notif)
}
}