fix: create ArtiProxy once and reuse — never recreate
The root cause of file lock conflicts was creating new ArtiProxy objects on each start. Even after stop() confirmed, build() could race with OS-level lock release. Now ArtiProxy is created once in the TorService constructor and reused for the app's lifetime. start() and stop() just toggle it on/off on the same instance. No more file lock conflicts. https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
This commit is contained in:
@@ -37,30 +37,23 @@ 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 STOP_TIMEOUT_MS = 10_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.
|
||||||
*
|
*
|
||||||
* ArtiProxy holds an exclusive lock on state files in the filesystem. Unlike
|
* ArtiProxy is created once and reused for the lifetime of the app.
|
||||||
* the old Android TorService (where bind/unbind was idempotent), we cannot
|
* Only [start] and [stop] are called on it — never recreated — to
|
||||||
* create and destroy ArtiProxy instances on every flow collection cycle —
|
* avoid state file lock conflicts in the native layer.
|
||||||
* the file lock from the old instance may not be released before the new
|
|
||||||
* one tries to acquire it.
|
|
||||||
*
|
|
||||||
* Instead, TorService owns a single ArtiProxy and exposes its state via
|
|
||||||
* a [StateFlow]. TorManager calls [start]/[stop] to control the lifecycle.
|
|
||||||
*/
|
*/
|
||||||
class TorService(
|
class TorService(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
) {
|
) {
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private var artiProxy: ArtiProxy? = null
|
private val running = AtomicBoolean(false)
|
||||||
private var currentPort: Int = DEFAULT_SOCKS_PORT
|
|
||||||
private val bootstrapped = AtomicBoolean(false)
|
private val bootstrapped = AtomicBoolean(false)
|
||||||
|
private val socksPort = DEFAULT_SOCKS_PORT
|
||||||
|
|
||||||
// Signalled by the log listener when Arti confirms it has stopped
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var stoppedSignal: CompletableDeferred<Unit>? = null
|
private var stoppedSignal: CompletableDeferred<Unit>? = null
|
||||||
|
|
||||||
@@ -76,13 +69,14 @@ class TorService(
|
|||||||
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 (bootstrapped.compareAndSet(false, true)) {
|
if (bootstrapped.compareAndSet(false, true)) {
|
||||||
_status.value = TorServiceStatus.Active(currentPort)
|
_status.value = TorServiceStatus.Active(socksPort)
|
||||||
Log.d("TorService") { "Arti bootstrapped on port $currentPort" }
|
Log.d("TorService") { "Arti bootstrapped on port $socksPort" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text.contains("state changed to Stopped", ignoreCase = true) -> {
|
text.contains("state changed to Stopped", ignoreCase = true) -> {
|
||||||
bootstrapped.set(false)
|
bootstrapped.set(false)
|
||||||
|
running.set(false)
|
||||||
_status.value = TorServiceStatus.Off
|
_status.value = TorServiceStatus.Off
|
||||||
stoppedSignal?.complete(Unit)
|
stoppedSignal?.complete(Unit)
|
||||||
}
|
}
|
||||||
@@ -98,16 +92,20 @@ class TorService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val artiProxy: ArtiProxy =
|
||||||
|
ArtiProxy
|
||||||
|
.Builder(context.applicationContext)
|
||||||
|
.setSocksPort(socksPort)
|
||||||
|
.setDnsPort(socksPort + 1)
|
||||||
|
.setLogListener(logListener)
|
||||||
|
.build()
|
||||||
|
|
||||||
suspend fun start() {
|
suspend fun start() {
|
||||||
// NonCancellable ensures that if the calling coroutine is cancelled
|
|
||||||
// (e.g., by transformLatest switching modes), we don't leak a
|
|
||||||
// half-started ArtiProxy with no reference to stop it.
|
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
if (artiProxy != null) {
|
if (running.get()) {
|
||||||
// Already running — just ensure status is up to date
|
|
||||||
if (bootstrapped.get()) {
|
if (bootstrapped.get()) {
|
||||||
_status.value = TorServiceStatus.Active(currentPort)
|
_status.value = TorServiceStatus.Active(socksPort)
|
||||||
} else {
|
} else {
|
||||||
_status.value = TorServiceStatus.Connecting
|
_status.value = TorServiceStatus.Connecting
|
||||||
}
|
}
|
||||||
@@ -118,36 +116,12 @@ class TorService(
|
|||||||
bootstrapped.set(false)
|
bootstrapped.set(false)
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
var socksPort = DEFAULT_SOCKS_PORT
|
try {
|
||||||
var lastError: Exception? = null
|
artiProxy.start()
|
||||||
|
running.set(true)
|
||||||
for (attempt in 0 until MAX_PORT_RETRIES) {
|
Log.d("TorService") { "Arti started on port $socksPort" }
|
||||||
try {
|
} catch (e: Exception) {
|
||||||
val proxy =
|
Log.e("TorService") { "Failed to start Arti: ${e.message}" }
|
||||||
ArtiProxy
|
|
||||||
.Builder(context.applicationContext)
|
|
||||||
.setSocksPort(socksPort)
|
|
||||||
.setDnsPort(socksPort + 1)
|
|
||||||
.setLogListener(logListener)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
proxy.start()
|
|
||||||
artiProxy = proxy
|
|
||||||
currentPort = socksPort
|
|
||||||
lastError = null
|
|
||||||
Log.d("TorService") { "Arti started on port $socksPort" }
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
lastError = e
|
|
||||||
Log.e("TorService") {
|
|
||||||
"Failed to start Arti on port $socksPort (attempt ${attempt + 1}): ${e.message}"
|
|
||||||
}
|
|
||||||
socksPort++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastError != null) {
|
|
||||||
Log.e("TorService") { "Failed to start Arti after $MAX_PORT_RETRIES attempts" }
|
|
||||||
_status.value = TorServiceStatus.Off
|
_status.value = TorServiceStatus.Off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,27 +130,21 @@ class TorService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stop() {
|
suspend fun stop() {
|
||||||
// NonCancellable ensures stop() completes fully even if the caller
|
|
||||||
// is cancelled, preventing leaked ArtiProxy instances and file locks.
|
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val proxy = artiProxy ?: return@withContext
|
if (!running.get()) return@withContext
|
||||||
artiProxy = null
|
|
||||||
bootstrapped.set(false)
|
|
||||||
|
|
||||||
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>()
|
val signal = CompletableDeferred<Unit>()
|
||||||
stoppedSignal = signal
|
stoppedSignal = signal
|
||||||
|
|
||||||
try {
|
try {
|
||||||
proxy.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}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the native layer to confirm stop and release file locks
|
|
||||||
val confirmed = withTimeoutOrNull(STOP_TIMEOUT_MS) { signal.await() }
|
val confirmed = withTimeoutOrNull(STOP_TIMEOUT_MS) { signal.await() }
|
||||||
stoppedSignal = null
|
stoppedSignal = null
|
||||||
|
|
||||||
@@ -184,6 +152,7 @@ class TorService(
|
|||||||
Log.d("TorService") { "Arti confirmed stopped" }
|
Log.d("TorService") { "Arti confirmed stopped" }
|
||||||
} else {
|
} else {
|
||||||
Log.w("TorService") { "Arti stop timed out after ${STOP_TIMEOUT_MS / 1000}s" }
|
Log.w("TorService") { "Arti stop timed out after ${STOP_TIMEOUT_MS / 1000}s" }
|
||||||
|
running.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_status.value = TorServiceStatus.Off
|
_status.value = TorServiceStatus.Off
|
||||||
|
|||||||
Reference in New Issue
Block a user