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 info.guardianproject.arti.ArtiProxy
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
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 DEFAULT_SOCKS_PORT = 19050
|
||||||
private const val MAX_PORT_RETRIES = 3
|
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(
|
class TorService(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
@@ -42,25 +48,35 @@ class TorService(
|
|||||||
|
|
||||||
var socksPort = DEFAULT_SOCKS_PORT
|
var socksPort = DEFAULT_SOCKS_PORT
|
||||||
var artiProxy: ArtiProxy? = null
|
var artiProxy: ArtiProxy? = null
|
||||||
var started = false
|
val bootstrapped = AtomicBoolean(false)
|
||||||
|
val lastLogTime = AtomicLong(System.currentTimeMillis())
|
||||||
|
|
||||||
val logListener =
|
val logListener =
|
||||||
ArtiLogListener { logLine ->
|
ArtiLogListener { logLine ->
|
||||||
val text = logLine ?: return@ArtiLogListener
|
val text = logLine ?: return@ArtiLogListener
|
||||||
Log.d("TorService") { "Arti: $text" }
|
Log.d("TorService") { "Arti: $text" }
|
||||||
|
lastLogTime.set(System.currentTimeMillis())
|
||||||
|
|
||||||
when {
|
when {
|
||||||
text.contains("Sufficiently bootstrapped", ignoreCase = true) ||
|
text.contains("Sufficiently bootstrapped", ignoreCase = true) ||
|
||||||
text.contains("is usable", ignoreCase = true) -> {
|
text.contains("is usable", ignoreCase = true) -> {
|
||||||
if (!started) {
|
if (bootstrapped.compareAndSet(false, true)) {
|
||||||
started = true
|
|
||||||
trySend(TorServiceStatus.Active(socksPort))
|
trySend(TorServiceStatus.Active(socksPort))
|
||||||
Log.d("TorService") { "Arti bootstrapped on port $socksPort" }
|
Log.d("TorService") { "Arti bootstrapped on port $socksPort" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text.contains("state changed to Stopped", ignoreCase = true) -> {
|
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)
|
trySend(TorServiceStatus.Off)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +98,15 @@ class TorService(
|
|||||||
break
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
lastError = e
|
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++
|
socksPort++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,14 +116,61 @@ class TorService(
|
|||||||
trySend(TorServiceStatus.Off)
|
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 {
|
awaitClose {
|
||||||
Log.d("TorService", "Stopping Arti Tor Service")
|
Log.d("TorService", "Stopping Arti Tor Service")
|
||||||
|
monitorJob.cancel()
|
||||||
try {
|
try {
|
||||||
artiProxy?.stop()
|
artiProxy?.stop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d("TorService") { "Failed to stop Arti: ${e.message}" }
|
Log.d("TorService") { "Failed to stop Arti: ${e.message}" }
|
||||||
}
|
}
|
||||||
trySend(TorServiceStatus.Off)
|
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user