fix(tor): set Active deterministically so the bootstrap callback can't race it

TorService.start() drove the Connecting→Active transition off Arti's
"Sufficiently bootstrapped" log line, gated on proxyRunning.get(). That line
is emitted from a tokio task spawned inside the native startSocksProxy
(arti-android-wrapper/lib.rs), which races startSocksProxy returning and
start() setting proxyRunning = true. When the task wins, the guard is false,
the transition is dropped, status stays Connecting, and the 60s
connection-failure screen fires even though the SOCKS proxy is up and
bootstrapped.

Set _status.value = Active(socksPort) directly in start() right after the
proxy binds and proxyRunning is set — under the lifecycleMutex start()
already holds, so a concurrent reset/stop can't clobber it. Reduce the log
callback to plain log forwarding now that it no longer drives status.

Verified on emulator-5554: Active now logged from start() before the
"Sufficiently bootstrapped" callback, zero ECONNREFUSED on the 9050 fallback
port, relays connecting through Tor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-16 17:29:41 -04:00
co-authored by Claude Opus 4.8
parent 0572b0091c
commit b7dadf8ec7
@@ -185,6 +185,11 @@ class TorService(
// setLogCallback and initialize are the first ArtiNative calls,
// which triggers System.loadLibrary on this IO thread.
if (initialized.compareAndSet(false, true)) {
// Forward Arti's internal logs. The Active transition is NOT driven
// from here: the "Sufficiently bootstrapped" line is emitted from a
// tokio task that races startSocksProxy returning, so honoring it here
// (gated on proxyRunning) intermittently dropped the transition. start()
// now sets Active deterministically once the proxy is bound.
ArtiNative.setLogCallback { text ->
Log.d("TorService") {
val newLine = text.indexOf('\n')
@@ -194,21 +199,6 @@ class TorService(
"Arti: $text"
}
}
when {
text.contains("Sufficiently bootstrapped", ignoreCase = true) -> {
// Only honor the bootstrap signal while the proxy is
// actually running. A reset/stop flips proxyRunning to
// false; a late callback from a torn-down client must
// not resurrect a stale Active status.
if (proxyRunning.get()) {
_status.value = TorServiceStatus.Active(socksPort)
val startedAt = bootstrapStartedAtMs
val elapsed = if (startedAt > 0) System.currentTimeMillis() - startedAt else -1
Log.d("TorService") { "Arti SOCKS proxy active on port $socksPort (bootstrap took ${elapsed}ms)" }
}
}
}
}
// Preserve the consensus/descriptor cache across cold starts. We used to
@@ -283,6 +273,22 @@ class TorService(
}
proxyRunning.set(true)
// Transition to Active deterministically here rather than relying on
// the "Sufficiently bootstrapped" log callback. That callback is emitted
// from a tokio task spawned inside the native startSocksProxy (see
// arti-android-wrapper lib.rs), which races the native call returning and
// this `proxyRunning.set(true)` above. When the task wins the race, the
// callback's `proxyRunning.get()` guard is still false, the Active
// transition is dropped, and status stays Connecting until the 60s
// connection-failure dialog fires. The client is bootstrapped (initialize
// returned 0) and the SOCKS proxy is bound, so this is the correct point
// to declare Active — and we hold lifecycleMutex, so a concurrent
// reset/stop can't clobber it.
val startedAt = bootstrapStartedAtMs
val elapsed = if (startedAt > 0) System.currentTimeMillis() - startedAt else -1
_status.value = TorServiceStatus.Active(socksPort)
Log.d("TorService") { "Arti SOCKS proxy active on port $socksPort (bootstrap took ${elapsed}ms)" }
}
}