Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BIGFRED_OCI_TAG: ${{ vars.BIGFRED_OCI_TAG }}
MICROINIT_OCI_TAG: ${{ vars.MICROINIT_OCI_TAG }}
run: |
set -euo pipefail
TAG="${BIGFRED_OCI_TAG:-main}"
echo "Using loco-server OCI tag: ${TAG}"
make native-prebuilt BIGFRED_OCI_TAG="${TAG}"
LOCO_TAG="${BIGFRED_OCI_TAG:-main}"
MICROINIT_TAG="${MICROINIT_OCI_TAG:-main}"
echo "Using loco-server OCI tag: ${LOCO_TAG}"
echo "Using microinit OCI tag: ${MICROINIT_TAG}"
make native-prebuilt BIGFRED_OCI_TAG="${LOCO_TAG}" MICROINIT_OCI_TAG="${MICROINIT_TAG}"
ls -lh native-prebuilt/arm64-v8a/

- name: Resolve app version
Expand Down Expand Up @@ -126,7 +129,7 @@ jobs:
APK="app/build/outputs/apk/release/app-release.apk"
unzip -l "$APK" | grep -E 'lib/arm64-v8a/libloco-server\.so'
unzip -l "$APK" | grep -E 'lib/arm64-v8a/libvalkey-server\.so'
unzip -l "$APK" | grep -E 'lib/arm64-v8a/libsupervisord\.so'
unzip -l "$APK" | grep -E 'lib/arm64-v8a/libmicroinit\.so'

- name: Verify APK version metadata
run: |
Expand Down
28 changes: 13 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# make import-models — fetch hydrus.pl catalog into assets/models/
# make loco-android — download libloco-server.so from GHCR (ORAS)
# make valkey-android — download libvalkey-server.so from deps-android-valkey latest release
# make supervisord-android — download supervisord libs from deps-android-supervisord latest release
# make microinit-android — download libmicroinit.so from GHCR (ORAS)

GRADLE ?= ./gradlew
GRADLE_FLAGS ?= --quiet
Expand All @@ -24,17 +24,17 @@ NATIVE_PREBUILT := native-prebuilt/arm64-v8a
LOCO_SO := $(NATIVE_PREBUILT)/libloco-server.so
LOCAL_LOCO_BIN := ../bigfred/bin/loco-server-android-arm64
VALKEY_SO := $(NATIVE_PREBUILT)/libvalkey-server.so
SUPERVISORD_SO := $(NATIVE_PREBUILT)/libsupervisord.so
SUPERVISORCTL_SO := $(NATIVE_PREBUILT)/libsupervisorctl.so
MICROINIT_SO := $(NATIVE_PREBUILT)/libmicroinit.so

BIGFRED_OCI_IMAGE ?= ghcr.io/dcc-bigfred/loco-server-android-arm64
BIGFRED_OCI_TAG ?= main
MICROINIT_OCI_IMAGE ?= ghcr.io/dcc-bigfred/microinit-android-arm64
MICROINIT_OCI_TAG ?= main

VALKEY_REPO ?= dcc-bigfred/deps-android-valkey
SUPERVISORD_REPO ?= dcc-bigfred/deps-android-supervisord

.PHONY: help apk release test test-android debug clean import-models \
loco-android valkey-android supervisord-android native-prebuilt
loco-android valkey-android microinit-android native-prebuilt

help:
@echo "Targets:"
Expand All @@ -46,7 +46,7 @@ help:
@echo " make import-models Import hydrus models DB + thumbs → $(ASSETS_MODELS)"
@echo " make loco-android Fetch $(LOCO_SO) (local $(LOCAL_LOCO_BIN) if present, else $(BIGFRED_OCI_IMAGE):$(BIGFRED_OCI_TAG); FORCE=1)"
@echo " make valkey-android Fetch $(VALKEY_SO) from $(VALKEY_REPO) latest release (skip if exists; FORCE=1)"
@echo " make supervisord-android Fetch supervisord libs from $(SUPERVISORD_REPO) latest release (skip if exists; FORCE=1)"
@echo " make microinit-android Fetch $(MICROINIT_SO) from $(MICROINIT_OCI_IMAGE):$(MICROINIT_OCI_TAG) (skip if exists; FORCE=1)"
@echo " make clean Clean Gradle build outputs"
@echo ""
@echo "Release signing (optional; falls back to debug keystore):"
Expand All @@ -71,10 +71,10 @@ import-models:
ifdef FORCE
.PHONY: force-clean-native
force-clean-native:
rm -f "$(LOCO_SO)" "$(VALKEY_SO)" "$(SUPERVISORD_SO)" "$(SUPERVISORCTL_SO)"
rm -f "$(LOCO_SO)" "$(VALKEY_SO)" "$(MICROINIT_SO)"
loco-android: force-clean-native
valkey-android: force-clean-native
supervisord-android: force-clean-native
microinit-android: force-clean-native
endif

loco-android: $(LOCO_SO)
Expand All @@ -95,15 +95,13 @@ valkey-android: $(VALKEY_SO)
$(VALKEY_SO):
./scripts/fetch-github-release-asset.sh "$(VALKEY_REPO)" libvalkey-server.so "$@"

supervisord-android: $(SUPERVISORD_SO) $(SUPERVISORCTL_SO)
microinit-android: $(MICROINIT_SO)

$(SUPERVISORD_SO):
./scripts/fetch-github-release-asset.sh "$(SUPERVISORD_REPO)" libsupervisord.so "$@"

$(SUPERVISORCTL_SO):
./scripts/fetch-github-release-asset.sh "$(SUPERVISORD_REPO)" libsupervisorctl.so "$@"
$(MICROINIT_SO):
@mkdir -p "$(NATIVE_PREBUILT)"
./scripts/fetch-ghcr-oras.sh "$(MICROINIT_OCI_IMAGE)" "$(MICROINIT_OCI_TAG)" "$@" main

native-prebuilt: loco-android valkey-android supervisord-android
native-prebuilt: loco-android valkey-android microinit-android

apk release: native-prebuilt
$(GRADLE) $(GRADLE_FLAGS) :app:assembleRelease
Expand Down
39 changes: 28 additions & 11 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ val fetchNativeBinaries by tasks.registering {
doLast {
val outDir = jniOut.asFile
outDir.mkdirs()
File(outDir, "libsupervisord.so").delete()
File(outDir, "libsupervisorctl.so").delete()

fun githubToken(): String? =
System.getenv("BIGFRED_NATIVE_TOKEN")
Expand Down Expand Up @@ -214,22 +216,12 @@ val fetchNativeBinaries by tasks.registering {
dest.copyTo(cache, overwrite = true)
}

// Valkey / supervisord: prefer make-fetched native-prebuilt, else latest GitHub release.
// Valkey: prefer make-fetched native-prebuilt, else latest GitHub release.
stageOrFetch(
"libvalkey-server.so",
"depsAndroidValkeyRepo",
"dcc-bigfred/deps-android-valkey",
)
stageOrFetch(
"libsupervisord.so",
"depsAndroidSupervisordRepo",
"dcc-bigfred/deps-android-supervisord",
)
stageOrFetch(
"libsupervisorctl.so",
"depsAndroidSupervisordRepo",
"dcc-bigfred/deps-android-supervisord",
)

// loco-server: local ../bigfred/bin → native-prebuilt → GHCR (ORAS).
val locoDest = File(outDir, "libloco-server.so")
Expand Down Expand Up @@ -257,6 +249,31 @@ val fetchNativeBinaries by tasks.registering {
"or pull from ghcr.io/dcc-bigfred/loco-server-android-arm64 (ORAS).",
)
}

// microinit: native-prebuilt → GHCR (ORAS).
val microinitDest = File(outDir, "libmicroinit.so")
val microinitCached = prebuilt.file("libmicroinit.so").asFile
when {
microinitCached.isFile -> {
microinitCached.copyTo(microinitDest, overwrite = true)
println("Staged libmicroinit.so from ${microinitCached.absolutePath}")
}
else -> {
val image = System.getenv("MICROINIT_OCI_IMAGE")
?: "ghcr.io/dcc-bigfred/microinit-android-arm64"
val tag = System.getenv("MICROINIT_OCI_TAG") ?: "main"
runScript(ghcrScript.asFile, image, tag, microinitDest.absolutePath, "main")
microinitCached.parentFile.mkdirs()
microinitDest.copyTo(microinitCached, overwrite = true)
}
}
if (!microinitDest.isFile || microinitDest.length() < 1024) {
throw GradleException(
"libmicroinit.so missing after fetch. " +
"Place it in native-prebuilt/arm64-v8a/ or pull from " +
"ghcr.io/dcc-bigfred/microinit-android-arm64 (ORAS).",
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ data class LocalServerPaths(
val etcDir: File get() = File(dataDir, "etc")
val runDir: File get() = File(dataDir, "run")
val logsDir: File get() = File(dataDir, "logs")
val redisDir: File get() = File(dataDir, "redis")
val redisDir: File get() = File(dataDir, "var/lib/redis")

val locoServerPid: File get() = File(runDir, "loco-server.pid")
val valkeyPid: File get() = File(runDir, "valkey.pid")
val supervisordPid: File get() = File(runDir, "supervisord.pid")
val microinitSocket: File get() = File(runDir, "microinit.sock")
val microinitPid: File get() = File(runDir, "microinit.pid")
val dbFile: File get() = File(dataDir, "bigfred.db")

fun ensureDirs() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.runBlocking

/**
* Foreground service hosting Valkey + loco-server for "BigFred on phone".
* Stage 1 uses `--no-supervisor`; stage 2 passes absolute jniLibs paths for supervisord.
* Foreground service hosting loco-server and its microinit-managed children for "BigFred on phone".
*/
class LocoServerService : Service() {

private var wakeLock: PowerManager.WakeLock? = null
private var valkeyProcess: Process? = null
private var locoProcess: Process? = null
private var watchdogThread: Thread? = null
private var bootThread: Thread? = null
Expand Down Expand Up @@ -147,8 +145,7 @@ class LocoServerService : Service() {
paths.ensureDirs()

ProcessOrphanReaper.reap(paths.locoServerPid, NativeBinaries.LOCO_SERVER)
ProcessOrphanReaper.reap(paths.valkeyPid, NativeBinaries.VALKEY)
ProcessOrphanReaper.reap(paths.supervisordPid, NativeBinaries.SUPERVISORD)
ProcessOrphanReaper.reap(paths.microinitPid, NativeBinaries.MICROINIT)
ensureBootActive()

if (ProcessOrphanReaper.isPortOpen("127.0.0.1", HTTP_PORT) &&
Expand All @@ -161,9 +158,7 @@ class LocoServerService : Service() {

val locoBin = NativeBinaries.require(this, NativeBinaries.LOCO_SERVER)
val valkeyBin = NativeBinaries.require(this, NativeBinaries.VALKEY)
val supervisordBin = NativeBinaries.file(this, NativeBinaries.SUPERVISORD)
val supervisorctlBin = NativeBinaries.file(this, NativeBinaries.SUPERVISORCTL)
val supervised = supervisordBin.isFile && supervisorctlBin.isFile
val microinitBin = NativeBinaries.require(this, NativeBinaries.MICROINIT)

val prefs = (application as BigFredApplication).serverPreferences
val jwt = runBlocking { prefs.getOrCreateLocalJwtSecret() }
Expand All @@ -175,15 +170,6 @@ class LocoServerService : Service() {
env["BIGFRED_LAN_PREFIX"] = prefix
Log.i(TAG, "BIGFRED_LAN_PREFIX=$prefix (for dcc-bus scan --lan-prefix)")
}
if (supervised) {
// Shim resolves sibling libsupervisord.so; also set env for clarity.
env["SUPERVISORD_BIN"] = supervisordBin.absolutePath
}

if (!supervised) {
// Stage 1: FGS owns Valkey; loco-server runs without supervisord.
bootUnmanagedValkey(paths, valkeyBin)
}

ensureBootActive()
val locoLog = File(paths.logsDir, "loco-server.log")
Expand All @@ -193,20 +179,10 @@ class LocoServerService : Service() {
"--db", paths.dbFile.absolutePath,
"--redis-bin", valkeyBin.absolutePath,
"--redis-addr", "127.0.0.1:$REDIS_PORT",
"--microinit-bin", microinitBin.absolutePath,
"--microinit-socket", paths.microinitSocket.absolutePath,
"--mdns=false",
)
if (supervised) {
// Stage 2: loco-server owns supervisord → Valkey + dcc-bus.
// Execute from nativeLibraryDir (executable); never copy to code_cache (noexec).
locoArgs += listOf(
"--supervisord-bin", supervisordBin.absolutePath,
"--supervisorctl-bin", supervisorctlBin.absolutePath,
)
Log.i(TAG, "starting supervised local mode (jniLibs supervisord)")
} else {
locoArgs += listOf("--redis-external", "--no-supervisor")
Log.i(TAG, "starting unmanaged local mode (--no-supervisor)")
}

locoProcess = ProcessBuilder(locoArgs)
.directory(paths.dataDir)
Expand All @@ -226,27 +202,7 @@ class LocoServerService : Service() {
}
running.set(true)
_state.value = LocalServerState.Running(LOCAL_BASE_URL)
startWatchdog(supervised)
}

private fun bootUnmanagedValkey(paths: LocalServerPaths, valkeyBin: File) {
ensureBootActive()
val valkeyLog = File(paths.logsDir, "valkey.log")
valkeyProcess = ProcessBuilder(
valkeyBin.absolutePath,
"--bind", "127.0.0.1",
"--port", REDIS_PORT.toString(),
"--dir", paths.redisDir.absolutePath,
"--save", "",
"--appendonly", "no",
"--protected-mode", "no",
"--daemonize", "no",
).redirectErrorStream(true)
.redirectOutput(ProcessBuilder.Redirect.appendTo(valkeyLog))
.start()
ensureBootActive()
ProcessOrphanReaper.writePid(paths.valkeyPid, processPid(valkeyProcess!!))
waitForPort("127.0.0.1", REDIS_PORT, 15_000)
startWatchdog()
}

private fun ensureBootActive() {
Expand All @@ -255,15 +211,6 @@ class LocoServerService : Service() {
}
}

private fun waitForPort(host: String, port: Int, timeoutMs: Long) {
val deadline = System.currentTimeMillis() + timeoutMs
while (System.currentTimeMillis() < deadline) {
if (ProcessOrphanReaper.isPortOpen(host, port)) return
Thread.sleep(200)
}
throw IllegalStateException("Timeout waiting for $host:$port")
}

private fun waitForHttpReady(timeoutMs: Long) {
val probe = ServerProbe()
val deadline = System.currentTimeMillis() + timeoutMs
Expand All @@ -275,16 +222,15 @@ class LocoServerService : Service() {
throw IllegalStateException("Timeout waiting for $LOCAL_BASE_URL")
}

private fun startWatchdog(supervised: Boolean) {
private fun startWatchdog() {
watchdogThread?.interrupt()
watchdogThread = thread(name = "loco-server-watchdog", isDaemon = true) {
while (running.get() && !Thread.currentThread().isInterrupted) {
try {
Thread.sleep(3_000)
val locoAlive = locoProcess?.isAlive == true
val valkeyOk = supervised || valkeyProcess?.isAlive == true
if (!locoAlive || !valkeyOk) {
Log.w(TAG, "child died loco=$locoAlive valkeyOk=$valkeyOk — restarting")
if (!locoAlive) {
Log.w(TAG, "loco-server died — restarting")
cleanupChildren()
running.set(false)
if (!booting.compareAndSet(false, true)) {
Expand Down Expand Up @@ -328,23 +274,34 @@ class LocoServerService : Service() {

@Synchronized
private fun cleanupChildren() {
listOf(locoProcess, valkeyProcess).forEach { proc ->
proc ?: return@forEach
val paths = LocalServerPaths.from(this)
locoProcess?.let { proc ->
try {
// SIGTERM loco-server first; its own shutdown defer forwards a
// soft-kill (SIGTERM) to microinit via the SDK. Give it a
// generous window so the managed services (redis/alloy/dcc-bus)
// can flush before loco-server is force-killed. Match the 15s
// microinit/SDK shutdown budget.
proc.destroy()
if (!proc.waitFor(3, java.util.concurrent.TimeUnit.SECONDS)) {
if (!proc.waitFor(15, java.util.concurrent.TimeUnit.SECONDS)) {
proc.destroyForcibly()
}
} catch (_: Exception) {
}
}
locoProcess = null
valkeyProcess = null
// Belt-and-suspenders: if loco-server was hard-killed before its
// graceful shutdown completed, microinit could be orphaned. Send it
// a SIGTERM directly (15s grace then SIGKILL) so its managed services
// still get a chance to stop cleanly. Relies on microinit writing
// run/microinit.pid (pid + starttime) via the Go supervise Host.
try {
ProcessOrphanReaper.stopGracefully(paths.microinitPid, NativeBinaries.MICROINIT, 15_000)
} catch (e: Exception) {
Log.w(TAG, "microinit graceful stop failed", e)
}
try {
val paths = LocalServerPaths.from(this)
paths.locoServerPid.delete()
paths.valkeyPid.delete()
paths.supervisordPid.delete()
} catch (_: Exception) {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import java.io.File
object NativeBinaries {
const val LOCO_SERVER = "libloco-server.so"
const val VALKEY = "libvalkey-server.so"
const val SUPERVISORD = "libsupervisord.so"
const val SUPERVISORCTL = "libsupervisorctl.so"
const val MICROINIT = "libmicroinit.so"

fun dir(context: Context): File =
File(context.applicationInfo.nativeLibraryDir)
Expand Down
Loading
Loading