From 293fd39a68627de2c8c245d5911249b5fa8ff50f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 00:23:52 +0000 Subject: [PATCH] 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 --- .../amethyst/ui/tor/TorService.kt | 83 +++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt index 2c965de36..4e2e3e13b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorService.kt @@ -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) }