OverviewFilesCommitsIssuesDocsReleasesPeers

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

97 lines · kt ref:
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)
    }
}