Merge pull request 'feat(companion): 60s mesh probe window + session pre-warm from the VPN service' (#111) from feat/mesh-probe-window-prewarm into main
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m47s
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m47s
This commit is contained in:
commit
128b78d083
@ -11,8 +11,8 @@ android {
|
||||
applicationId = "com.archipelago.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 24
|
||||
versionName = "0.5.4"
|
||||
versionCode = 25
|
||||
versionName = "0.5.5"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
||||
@ -10,10 +10,15 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import com.archipelago.app.MainActivity
|
||||
import com.archipelago.app.R
|
||||
import com.archipelago.app.data.ServerPreferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@ -27,6 +32,7 @@ import kotlinx.coroutines.launch
|
||||
class ArchyVpnService : VpnService() {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var warmerJob: Job? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_STOP) {
|
||||
@ -89,10 +95,57 @@ class ArchyVpnService : VpnService() {
|
||||
val fd = pfd.detachFd()
|
||||
val result = FipsNative.start(identity.secret, peersJson, fd)
|
||||
Log.i(TAG, "mesh start: $result")
|
||||
if (result.contains("\"error\"")) shutdown()
|
||||
if (result.contains("\"error\"")) {
|
||||
shutdown()
|
||||
} else {
|
||||
startSessionWarmer()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-warm + keep-warm mesh sessions to every known node ULA.
|
||||
*
|
||||
* Discovery + first session through the public tree can take 15s+
|
||||
* (HANDOFF-2026-07-23 node diagnosis) — paying that cost here, the
|
||||
* moment the tunnel is up, means the connect probe and WebView hit an
|
||||
* established session instead of timing out on a cold one. The periodic
|
||||
* touch afterwards keeps the session from idling out. Failed connects
|
||||
* are expected and cheap; the attempt itself is what drives discovery.
|
||||
*/
|
||||
private fun startSessionWarmer() {
|
||||
warmerJob?.cancel()
|
||||
warmerJob = scope.launch {
|
||||
val prefs = ServerPreferences(this@ArchyVpnService)
|
||||
var round = 0
|
||||
while (isActive && FipsNative.isRunning()) {
|
||||
val ulas = try {
|
||||
prefs.savedServers.first().mapNotNull { it.meshIp.ifBlank { null } }.distinct()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
for (ula in ulas) {
|
||||
try {
|
||||
java.net.Socket().use { s ->
|
||||
s.connect(
|
||||
java.net.InetSocketAddress(java.net.InetAddress.getByName(ula), 80),
|
||||
20_000,
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Cold path / node away — the connect attempt still
|
||||
// drove session establishment; try again next round.
|
||||
}
|
||||
}
|
||||
round++
|
||||
// Aggressive for the first ~minute (session bring-up), then a
|
||||
// slow keep-warm tick that costs nearly nothing.
|
||||
delay(if (round < 12) 5_000 else 60_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shutdown() {
|
||||
warmerJob?.cancel()
|
||||
FipsNative.stop()
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
|
||||
@ -185,13 +185,17 @@ fun ServerConnectScreen(
|
||||
useHttps = false,
|
||||
port = "",
|
||||
)
|
||||
// The tunnel + mesh route need a moment on cold start — and on
|
||||
// a first-ever pairing the VPN consent dialog is on screen at
|
||||
// the same time, so give the user time to tap it.
|
||||
for (attempt in 0 until 6) {
|
||||
if (attempt > 0) delay(2000)
|
||||
reachable = testConnection(meshServer)
|
||||
if (reachable) break
|
||||
// Mesh discovery + first session can take 15s+ through the
|
||||
// public tree (HANDOFF-2026-07-23 node diagnosis), and on a
|
||||
// first-ever pairing the VPN consent dialog is on screen at
|
||||
// the same time — so probe patiently inside a 60s budget with
|
||||
// per-attempt timeouts wide enough to ride out TCP
|
||||
// retransmit backoff. The VPN service pre-warms the session
|
||||
// in parallel (ArchyVpnService.startSessionWarmer).
|
||||
val deadline = System.currentTimeMillis() + 60_000
|
||||
while (!reachable && System.currentTimeMillis() < deadline) {
|
||||
reachable = testConnection(meshServer, timeoutMs = 15_000)
|
||||
if (!reachable) delay(3000)
|
||||
}
|
||||
}
|
||||
isConnecting = false
|
||||
@ -673,8 +677,10 @@ private fun sanitizeAddress(input: String): String {
|
||||
.trimEnd('/')
|
||||
}
|
||||
|
||||
/** Test RPC connectivity. Accepts self-signed certs for local LAN servers. */
|
||||
private suspend fun testConnection(server: ServerEntry): Boolean {
|
||||
/** Test RPC connectivity. Accepts self-signed certs for local LAN servers.
|
||||
* [timeoutMs] is per-phase (connect / read) — mesh probes need far more
|
||||
* patience than LAN ones (first session through the tree can take 15s+). */
|
||||
private suspend fun testConnection(server: ServerEntry, timeoutMs: Int = 5000): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val url = URL("${server.toUrl()}/rpc/v1")
|
||||
@ -694,8 +700,8 @@ private suspend fun testConnection(server: ServerEntry): Boolean {
|
||||
}
|
||||
|
||||
connection.requestMethod = "POST"
|
||||
connection.connectTimeout = 5000
|
||||
connection.readTimeout = 5000
|
||||
connection.connectTimeout = timeoutMs
|
||||
connection.readTimeout = timeoutMs
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.doOutput = true
|
||||
val body = """{"method":"server.echo","params":{"message":"ping"}}"""
|
||||
|
||||
@ -126,3 +126,19 @@ Chain of findings, each verified live:
|
||||
broader public mesh.
|
||||
- **Upstream:** report the direct-peer session-path asymmetry to
|
||||
jmcorgan/fips (0.4.1).
|
||||
|
||||
## App-side recommendations implemented (23:30–23:50, Mac agent — 0.5.5/vc25)
|
||||
|
||||
- Connect probe: mesh ULA now probed inside a **60s budget** with 15s
|
||||
per-phase timeouts (rides out TCP retransmit backoff), replacing the old
|
||||
~8s window.
|
||||
- **Session pre-warm**: the VPN service starts probing every saved node ULA
|
||||
the moment the tunnel is up (5s cadence for the first minute, then a 60s
|
||||
keep-warm tick) — discovery/handshake cost is paid in the background, and
|
||||
the session never idles out while the mesh is connected.
|
||||
- Extra evidence for the infra decision: from the phone on 5G, a 25-packet
|
||||
ping burst to the node's ULA (25s continuous traffic, post-nginx-fix)
|
||||
still got **zero replies** — discovery through the public tree isn't just
|
||||
slow, it's failing outright. The private-tree option (detach fleet from
|
||||
the public v0l mesh, root at vps2) looks like the real fix; app-side
|
||||
patience only helps once discovery succeeds at all.
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user