fix: wait for Arti to confirm stop before allowing restart
The fixed 2s delay was insufficient — Arti's native layer releases file locks asynchronously. Now stop() waits for the actual "state changed to Stopped" log confirmation via CompletableDeferred, with a 10s timeout as safety net. This ensures the file lock is truly released before start() creates a new ArtiProxy instance. https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
This commit is contained in:
@@ -24,20 +24,21 @@ import android.content.Context
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import info.guardianproject.arti.ArtiLogListener
|
||||
import info.guardianproject.arti.ArtiProxy
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
private const val DEFAULT_SOCKS_PORT = 19050
|
||||
private const val MAX_PORT_RETRIES = 3
|
||||
private const val STOP_SETTLE_MS = 2_000L
|
||||
private const val STOP_TIMEOUT_MS = 10_000L
|
||||
|
||||
/**
|
||||
* Manages a single ArtiProxy instance with explicit start/stop lifecycle.
|
||||
@@ -59,6 +60,10 @@ class TorService(
|
||||
private var currentPort: Int = DEFAULT_SOCKS_PORT
|
||||
private val bootstrapped = AtomicBoolean(false)
|
||||
|
||||
// Signalled by the log listener when Arti confirms it has stopped
|
||||
@Volatile
|
||||
private var stoppedSignal: CompletableDeferred<Unit>? = null
|
||||
|
||||
private val _status = MutableStateFlow<TorServiceStatus>(TorServiceStatus.Off)
|
||||
val status: StateFlow<TorServiceStatus> = _status.asStateFlow()
|
||||
|
||||
@@ -79,6 +84,7 @@ class TorService(
|
||||
text.contains("state changed to Stopped", ignoreCase = true) -> {
|
||||
bootstrapped.set(false)
|
||||
_status.value = TorServiceStatus.Off
|
||||
stoppedSignal?.complete(Unit)
|
||||
}
|
||||
|
||||
text.contains(
|
||||
@@ -160,13 +166,25 @@ class TorService(
|
||||
|
||||
Log.d("TorService", "Stopping Arti")
|
||||
withContext(Dispatchers.IO) {
|
||||
// Set up a signal to wait for the "Stopped" log confirmation
|
||||
val signal = CompletableDeferred<Unit>()
|
||||
stoppedSignal = signal
|
||||
|
||||
try {
|
||||
proxy.stop()
|
||||
} catch (e: Exception) {
|
||||
Log.d("TorService") { "Failed to stop Arti: ${e.message}" }
|
||||
}
|
||||
// Give the native layer time to release file locks
|
||||
delay(STOP_SETTLE_MS)
|
||||
|
||||
// Wait for the native layer to confirm stop and release file locks
|
||||
val confirmed = withTimeoutOrNull(STOP_TIMEOUT_MS) { signal.await() }
|
||||
stoppedSignal = null
|
||||
|
||||
if (confirmed != null) {
|
||||
Log.d("TorService") { "Arti confirmed stopped" }
|
||||
} else {
|
||||
Log.w("TorService") { "Arti stop timed out after ${STOP_TIMEOUT_MS / 1000}s" }
|
||||
}
|
||||
}
|
||||
_status.value = TorServiceStatus.Off
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user