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 com.vitorpamplona.quartz.utils.Log
|
||||||
import info.guardianproject.arti.ArtiLogListener
|
import info.guardianproject.arti.ArtiLogListener
|
||||||
import info.guardianproject.arti.ArtiProxy
|
import info.guardianproject.arti.ArtiProxy
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
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 STOP_SETTLE_MS = 2_000L
|
private const val STOP_TIMEOUT_MS = 10_000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages a single ArtiProxy instance with explicit start/stop lifecycle.
|
* Manages a single ArtiProxy instance with explicit start/stop lifecycle.
|
||||||
@@ -59,6 +60,10 @@ class TorService(
|
|||||||
private var currentPort: Int = DEFAULT_SOCKS_PORT
|
private var currentPort: Int = DEFAULT_SOCKS_PORT
|
||||||
private val bootstrapped = AtomicBoolean(false)
|
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)
|
private val _status = MutableStateFlow<TorServiceStatus>(TorServiceStatus.Off)
|
||||||
val status: StateFlow<TorServiceStatus> = _status.asStateFlow()
|
val status: StateFlow<TorServiceStatus> = _status.asStateFlow()
|
||||||
|
|
||||||
@@ -79,6 +84,7 @@ class TorService(
|
|||||||
text.contains("state changed to Stopped", ignoreCase = true) -> {
|
text.contains("state changed to Stopped", ignoreCase = true) -> {
|
||||||
bootstrapped.set(false)
|
bootstrapped.set(false)
|
||||||
_status.value = TorServiceStatus.Off
|
_status.value = TorServiceStatus.Off
|
||||||
|
stoppedSignal?.complete(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
text.contains(
|
text.contains(
|
||||||
@@ -160,13 +166,25 @@ class TorService(
|
|||||||
|
|
||||||
Log.d("TorService", "Stopping Arti")
|
Log.d("TorService", "Stopping Arti")
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
// Set up a signal to wait for the "Stopped" log confirmation
|
||||||
|
val signal = CompletableDeferred<Unit>()
|
||||||
|
stoppedSignal = signal
|
||||||
|
|
||||||
try {
|
try {
|
||||||
proxy.stop()
|
proxy.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}" }
|
||||||
}
|
}
|
||||||
// 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
|
_status.value = TorServiceStatus.Off
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user