app: long runs that watch, and a relay for the port that keeps moving

Long mode starts listeners at t=0 and keeps them running past the
battery: a network-change watcher that finally fills networks[].changes[]
(defined since the schema's first draft, never populated), an RSSI log, a
ping series giving loss and jitter over minutes, and mDNS listening for
the whole window. This is the class of fault a short run cannot see - a
link that drops for four seconds between two probes is reported healthy
by both of them. run.mode records which question was asked, because
silence means different things in the two modes.

The adb relay replaces the retired beacon: AdbRelay watches adbd's own
mDNS with the resolve-once discipline the beacon learned the hard way
(resolving re-arms adbd and pops a notification), a foreground service
keeps it alive with the screen off, and the heartbeat re-posts the cached
endpoint rather than re-resolving. It exists because mDNS does not cross
subnets and the wireless-debug port rotates every few minutes.

Also records why LLDP/CDP cannot follow SSDP into long mode: both are raw
L2 frames, so they need CAP_NET_RAW - root tier, not app, and Shizuku's
shell user does not have it either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 14:43:33 +02:00
co-authored by Claude Opus 5
parent ae63bd7c7f
commit 0071e00003
24 changed files with 1834 additions and 107 deletions
@@ -209,6 +209,36 @@ class ControlClient(
}
}
/**
* Reports where adbd's wireless-debug listener can be reached on this device's LAN.
*
* Dev scaffolding, not a measurement: it exists because mDNS does not cross subnets, so a
* developer working from a different network cannot discover the port that rotates every few
* minutes. A device sitting on the test LAN can see it and say so. Deliberately not part of
* the probe protocol's capability set — the server documents it as a dev relay.
*/
fun reportAdbEndpoint(
credential: String,
host: String,
port: Int,
deviceName: String? = null,
note: String? = null,
): String {
val conn = open("/v1/devtools/adb-endpoint", "POST", credential)
val fields = buildString {
append("""{"host":${jstr(host)},"port":$port""")
deviceName?.let { append(""","device_name":${jstr(it)}""") }
note?.let { append(""","note":${jstr(it)}""") }
append("}")
}
writeJson(conn, fields)
val body = body(conn)
check(conn.responseCode in 200..299) {
"adb endpoint report failed: ${conn.responseCode} $body"
}
return body
}
/** Lists this device's runs stored on the server. */
fun listRuns(credential: String): String {
val conn = open("/v1/runs", "GET", credential)