fix: harden TorService with bootstrap timeout, thread safety, and error recovery

Address gaps found in code review:
- Use AtomicBoolean/AtomicLong for state accessed from native Arti
  log callback thread (was a data race)
- Add bootstrap timeout monitor (120s) that restarts Arti once if
  bootstrapping stalls, following BitChat's inactivity pattern
- Clean up failed ArtiProxy instances before port retry
- Detect "Another process has the lock" fatal error from Arti logs

https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
This commit is contained in:
Claude
2026-04-01 00:23:52 +00:00
parent 11af50e0ad
commit 293fd39a68
@@ -26,11 +26,17 @@ import info.guardianproject.arti.ArtiLogListener
import info.guardianproject.arti.ArtiProxy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
private const val DEFAULT_SOCKS_PORT = 19050
private const val MAX_PORT_RETRIES = 3
private const val BOOTSTRAP_TIMEOUT_MS = 120_000L
private const val BOOTSTRAP_CHECK_INTERVAL_MS = 10_000L
class TorService(
val context: Context,
@@ -42,25 +48,35 @@ class TorService(
var socksPort = DEFAULT_SOCKS_PORT
var artiProxy: ArtiProxy? = null
var started = false
val bootstrapped = AtomicBoolean(false)
val lastLogTime = AtomicLong(System.currentTimeMillis())
val logListener =
ArtiLogListener { logLine ->
val text = logLine ?: return@ArtiLogListener
Log.d("TorService") { "Arti: $text" }
lastLogTime.set(System.currentTimeMillis())
when {
text.contains("Sufficiently bootstrapped", ignoreCase = true) ||
text.contains("is usable", ignoreCase = true) -> {
if (!started) {
started = true
if (bootstrapped.compareAndSet(false, true)) {
trySend(TorServiceStatus.Active(socksPort))
Log.d("TorService") { "Arti bootstrapped on port $socksPort" }
}
}
text.contains("state changed to Stopped", ignoreCase = true) -> {
started = false
bootstrapped.set(false)
trySend(TorServiceStatus.Off)
}
text.contains(
"Another process has the lock",
ignoreCase = true,
) -> {
Log.e("TorService") { "Arti state file lock conflict" }
bootstrapped.set(false)
trySend(TorServiceStatus.Off)
}
}
@@ -82,7 +98,15 @@ class TorService(
break
} catch (e: Exception) {
lastError = e
Log.e("TorService") { "Failed to start Arti on port $socksPort (attempt ${attempt + 1}): ${e.message}" }
Log.e("TorService") {
"Failed to start Arti on port $socksPort (attempt ${attempt + 1}): ${e.message}"
}
// Stop the failed instance before retrying
try {
artiProxy?.stop()
} catch (_: Exception) {
}
artiProxy = null
socksPort++
}
}
@@ -92,14 +116,61 @@ class TorService(
trySend(TorServiceStatus.Off)
}
// Monitor for bootstrap stalls: if Arti stops producing logs
// during bootstrap, restart it.
val monitorJob =
launch {
val startTime = System.currentTimeMillis()
while (!bootstrapped.get()) {
delay(BOOTSTRAP_CHECK_INTERVAL_MS)
if (bootstrapped.get()) break
val elapsed = System.currentTimeMillis() - startTime
val timeSinceLastLog = System.currentTimeMillis() - lastLogTime.get()
if (elapsed > BOOTSTRAP_TIMEOUT_MS) {
Log.w("TorService") {
"Arti bootstrap timed out after ${elapsed / 1000}s"
}
// Stop and restart once
try {
artiProxy?.stop()
} catch (_: Exception) {
}
delay(1000)
try {
artiProxy =
ArtiProxy
.Builder(context.applicationContext)
.setSocksPort(socksPort)
.setDnsPort(socksPort + 1)
.setLogListener(logListener)
.build()
artiProxy!!.start()
lastLogTime.set(System.currentTimeMillis())
} catch (e: Exception) {
Log.e("TorService") { "Arti restart failed: ${e.message}" }
trySend(TorServiceStatus.Off)
}
break // Only retry once
} else if (timeSinceLastLog > BOOTSTRAP_CHECK_INTERVAL_MS * 3) {
Log.w("TorService") {
"Arti log stall detected (${timeSinceLastLog / 1000}s since last log)"
}
}
}
}
awaitClose {
Log.d("TorService", "Stopping Arti Tor Service")
monitorJob.cancel()
try {
artiProxy?.stop()
} catch (e: Exception) {
Log.d("TorService") { "Failed to stop Arti: ${e.message}" }
}
trySend(TorServiceStatus.Off)
}
}.flowOn(Dispatchers.IO)
}