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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user