mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
better tor management (#373)
This commit is contained in:
@@ -16,6 +16,8 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
|
||||
import java.net.InetSocketAddress
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
@@ -31,6 +33,7 @@ object TorManager {
|
||||
private const val RESTART_DELAY_MS = 2000L // 2 seconds between stop/start
|
||||
private const val INACTIVITY_TIMEOUT_MS = 5000L // 5 seconds of no activity before restart
|
||||
private const val MAX_RETRY_ATTEMPTS = 5
|
||||
private const val STOP_TIMEOUT_MS = 7000L
|
||||
|
||||
private val appScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
@@ -51,19 +54,24 @@ object TorManager {
|
||||
private enum class LifecycleState { STOPPED, STARTING, RUNNING, STOPPING }
|
||||
@Volatile private var lifecycleState: LifecycleState = LifecycleState.STOPPED
|
||||
|
||||
enum class TorState { OFF, STARTING, BOOTSTRAPPING, RUNNING, STOPPING, ERROR }
|
||||
|
||||
data class TorStatus(
|
||||
val mode: TorMode = TorMode.OFF,
|
||||
val running: Boolean = false,
|
||||
val bootstrapPercent: Int = 0,
|
||||
val lastLogLine: String = ""
|
||||
val bootstrapPercent: Int = 0, // kept for backwards compatibility with UI; 0 or 100 only
|
||||
val lastLogLine: String = "",
|
||||
val state: TorState = TorState.OFF
|
||||
)
|
||||
|
||||
private val _status = MutableStateFlow(TorStatus())
|
||||
val statusFlow: StateFlow<TorStatus> = _status.asStateFlow()
|
||||
|
||||
private val stateChangeDeferred = AtomicReference<CompletableDeferred<TorState>?>(null)
|
||||
|
||||
fun isProxyEnabled(): Boolean {
|
||||
val s = _status.value
|
||||
return s.mode != TorMode.OFF && s.running && s.bootstrapPercent >= 100 && socksAddr != null
|
||||
return s.mode != TorMode.OFF && s.running && s.bootstrapPercent >= 100 && socksAddr != null && s.state == TorState.RUNNING
|
||||
}
|
||||
|
||||
fun init(application: Application) {
|
||||
@@ -104,9 +112,12 @@ object TorManager {
|
||||
TorMode.OFF -> {
|
||||
Log.i(TAG, "applyMode: OFF -> stopping tor")
|
||||
lifecycleState = LifecycleState.STOPPING
|
||||
stopArti()
|
||||
_status.value = _status.value.copy(mode = TorMode.OFF, running = false, bootstrapPercent = 0, state = TorState.STOPPING)
|
||||
stopArti() // non-suspending immediate request
|
||||
// Best-effort wait for STOPPED before we declare OFF
|
||||
waitForStateTransition(target = TorState.OFF, timeoutMs = STOP_TIMEOUT_MS)
|
||||
socksAddr = null
|
||||
_status.value = _status.value.copy(mode = TorMode.OFF, running = false, bootstrapPercent = 0)
|
||||
_status.value = _status.value.copy(mode = TorMode.OFF, running = false, bootstrapPercent = 0, state = TorState.OFF)
|
||||
currentSocksPort = DEFAULT_SOCKS_PORT
|
||||
bindRetryAttempts = 0
|
||||
lifecycleState = LifecycleState.STOPPED
|
||||
@@ -124,8 +135,8 @@ object TorManager {
|
||||
}
|
||||
bindRetryAttempts = 0
|
||||
lifecycleState = LifecycleState.STARTING
|
||||
_status.value = _status.value.copy(mode = TorMode.ON, running = false, bootstrapPercent = 0, state = TorState.STARTING)
|
||||
startArti(application, useDelay = false)
|
||||
_status.value = _status.value.copy(mode = TorMode.ON)
|
||||
// Defer enabling proxy until bootstrap completes
|
||||
appScope.launch {
|
||||
waitUntilBootstrapped()
|
||||
@@ -146,7 +157,8 @@ object TorManager {
|
||||
|
||||
private suspend fun startArti(application: Application, useDelay: Boolean = false) {
|
||||
try {
|
||||
stopArtiInternal()
|
||||
// Ensure any previous instance is fully stopped before starting a new one
|
||||
stopArtiAndWait()
|
||||
|
||||
Log.i(TAG, "Starting Arti on port $currentSocksPort…")
|
||||
if (useDelay) {
|
||||
@@ -159,15 +171,7 @@ object TorManager {
|
||||
Log.i(TAG, "arti: $s")
|
||||
lastLogTime.set(System.currentTimeMillis())
|
||||
_status.value = _status.value.copy(lastLogLine = s)
|
||||
if (
|
||||
s.contains("Sufficiently bootstrapped", ignoreCase = true) ||
|
||||
s.contains("AMEx: state changed to Running", ignoreCase = true)
|
||||
) {
|
||||
_status.value = _status.value.copy(bootstrapPercent = 100)
|
||||
retryAttempts = 0 // Reset retry attempts on successful bootstrap
|
||||
bindRetryAttempts = 0 // Reset bind retry attempts on successful bootstrap
|
||||
startInactivityMonitoring()
|
||||
}
|
||||
handleArtiLogLine(s)
|
||||
}
|
||||
|
||||
val proxy = ArtiProxy.Builder(application)
|
||||
@@ -180,12 +184,13 @@ object TorManager {
|
||||
proxy.start()
|
||||
lastLogTime.set(System.currentTimeMillis())
|
||||
|
||||
_status.value = _status.value.copy(running = true, bootstrapPercent = 0)
|
||||
_status.value = _status.value.copy(running = true, bootstrapPercent = 0, state = TorState.STARTING)
|
||||
lifecycleState = LifecycleState.RUNNING
|
||||
startInactivityMonitoring()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error starting Arti on port $currentSocksPort: ${e.message}")
|
||||
_status.value = _status.value.copy(state = TorState.ERROR)
|
||||
|
||||
// Check if this is a bind error
|
||||
val isBindError = isBindError(e)
|
||||
@@ -198,7 +203,7 @@ object TorManager {
|
||||
} else if (isBindError) {
|
||||
Log.e(TAG, "Max bind retry attempts reached ($MAX_RETRY_ATTEMPTS), giving up")
|
||||
lifecycleState = LifecycleState.STOPPED
|
||||
_status.value = _status.value.copy(running = false, bootstrapPercent = 0)
|
||||
_status.value = _status.value.copy(running = false, bootstrapPercent = 0, state = TorState.ERROR)
|
||||
} else {
|
||||
// For non-bind errors, use the existing retry mechanism
|
||||
scheduleRetry(application)
|
||||
@@ -235,12 +240,21 @@ object TorManager {
|
||||
private fun stopArti() {
|
||||
stopArtiInternal()
|
||||
socksAddr = null
|
||||
_status.value = _status.value.copy(running = false, bootstrapPercent = 0)
|
||||
_status.value = _status.value.copy(running = false, bootstrapPercent = 0, state = TorState.STOPPING)
|
||||
}
|
||||
|
||||
private suspend fun stopArtiAndWait(timeoutMs: Long = STOP_TIMEOUT_MS) {
|
||||
// Request stop
|
||||
stopArtiInternal()
|
||||
// Wait for confirmation via logs (Stopped) or timeout
|
||||
waitForStateTransition(target = TorState.OFF, timeoutMs = timeoutMs)
|
||||
// Small grace period before relaunch to let file locks clear
|
||||
delay(200)
|
||||
}
|
||||
|
||||
private suspend fun restartArti(application: Application) {
|
||||
Log.i(TAG, "Restarting Arti (keeping SOCKS proxy enabled)...")
|
||||
stopArtiInternal()
|
||||
stopArtiAndWait()
|
||||
delay(RESTART_DELAY_MS)
|
||||
startArti(application, useDelay = false) // Already delayed above
|
||||
}
|
||||
@@ -302,23 +316,68 @@ object TorManager {
|
||||
retryJob = null
|
||||
}
|
||||
|
||||
// Removed Tor resource installation: not needed for Arti
|
||||
|
||||
/**
|
||||
* Build an execution command that works on Android 10+ where app data dirs are mounted noexec.
|
||||
* We invoke the platform dynamic linker and pass the PIE binary path as its first arg.
|
||||
*/
|
||||
// Removed exec command builder: not needed for Arti
|
||||
|
||||
private suspend fun waitUntilBootstrapped() {
|
||||
val current = _status.value
|
||||
if (!current.running) return
|
||||
if (current.bootstrapPercent >= 100) return
|
||||
// Suspend until we observe 100% at least once
|
||||
if (current.bootstrapPercent >= 100 && current.state == TorState.RUNNING) return
|
||||
// Suspend until we observe RUNNING at least once
|
||||
while (true) {
|
||||
val s = statusFlow.first { it.bootstrapPercent >= 100 || !it.running }
|
||||
if (!s.running) return
|
||||
if (s.bootstrapPercent >= 100) return
|
||||
val s = statusFlow.first { (it.bootstrapPercent >= 100 && it.state == TorState.RUNNING) || !it.running || it.state == TorState.ERROR }
|
||||
if (!s.running || s.state == TorState.ERROR) return
|
||||
if (s.bootstrapPercent >= 100 && s.state == TorState.RUNNING) return
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleArtiLogLine(s: String) {
|
||||
when {
|
||||
s.contains("AMEx: state changed to Initialized", ignoreCase = true) -> {
|
||||
_status.value = _status.value.copy(state = TorState.STARTING)
|
||||
completeWaitersIf(TorState.STARTING)
|
||||
}
|
||||
s.contains("AMEx: state changed to Starting", ignoreCase = true) -> {
|
||||
_status.value = _status.value.copy(state = TorState.STARTING)
|
||||
completeWaitersIf(TorState.STARTING)
|
||||
}
|
||||
s.contains("Sufficiently bootstrapped; system SOCKS now functional", ignoreCase = true) -> {
|
||||
_status.value = _status.value.copy(bootstrapPercent = 100, state = TorState.BOOTSTRAPPING)
|
||||
retryAttempts = 0
|
||||
bindRetryAttempts = 0
|
||||
startInactivityMonitoring()
|
||||
}
|
||||
s.contains("AMEx: state changed to Running", ignoreCase = true) -> {
|
||||
// If we already saw Sufficiently bootstrapped, mark as RUNNING and ready.
|
||||
val bp = if (_status.value.bootstrapPercent >= 100) 100 else 100 // treat Running as ready
|
||||
_status.value = _status.value.copy(state = TorState.RUNNING, bootstrapPercent = bp, running = true)
|
||||
completeWaitersIf(TorState.RUNNING)
|
||||
}
|
||||
s.contains("AMEx: state changed to Stopping", ignoreCase = true) -> {
|
||||
_status.value = _status.value.copy(state = TorState.STOPPING, running = false)
|
||||
}
|
||||
s.contains("AMEx: state changed to Stopped", ignoreCase = true) -> {
|
||||
_status.value = _status.value.copy(state = TorState.OFF, running = false, bootstrapPercent = 0)
|
||||
completeWaitersIf(TorState.OFF)
|
||||
}
|
||||
s.contains("Another process has the lock on our state files", ignoreCase = true) -> {
|
||||
// Signal error; we'll likely need to wait longer before restart
|
||||
_status.value = _status.value.copy(state = TorState.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun completeWaitersIf(state: TorState) {
|
||||
stateChangeDeferred.getAndSet(null)?.let { def ->
|
||||
def.complete(state)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForStateTransition(target: TorState, timeoutMs: Long): TorState? {
|
||||
val def = CompletableDeferred<TorState>()
|
||||
stateChangeDeferred.getAndSet(def)?.cancel()
|
||||
return withTimeoutOrNull(timeoutMs) {
|
||||
// Fast-path: if we're already there
|
||||
val cur = _status.value.state
|
||||
if (cur == target) return@withTimeoutOrNull cur
|
||||
def.await()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user