fix: keep ArtiProxy alive across mode switches to avoid file lock conflicts
ArtiProxy holds an exclusive filesystem lock. Destroying it on OFF and recreating on INTERNAL caused lock conflicts because the native layer needs time to release the lock. Instead, create ArtiProxy once and never destroy it. When Tor is OFF or EXTERNAL, the proxy sits idle with no SOCKS connections — negligible resource usage. This eliminates all file lock race conditions. Also wrap start/stop in NonCancellable to prevent transformLatest cancellation from leaking half-initialized proxy instances. https://claude.ai/code/session_01BApgDd5udqBzMqysSRMpZu
This commit is contained in:
@@ -63,12 +63,10 @@ class TorManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
TorType.OFF -> {
|
TorType.OFF -> {
|
||||||
service.stop()
|
|
||||||
emit(TorServiceStatus.Off)
|
emit(TorServiceStatus.Off)
|
||||||
}
|
}
|
||||||
|
|
||||||
TorType.EXTERNAL -> {
|
TorType.EXTERNAL -> {
|
||||||
service.stop()
|
|
||||||
if (externalSocksPort > 0) {
|
if (externalSocksPort > 0) {
|
||||||
emit(TorServiceStatus.Active(externalSocksPort))
|
emit(TorServiceStatus.Active(externalSocksPort))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -95,75 +96,84 @@ class TorService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun start() {
|
suspend fun start() {
|
||||||
mutex.withLock {
|
// NonCancellable ensures that if the calling coroutine is cancelled
|
||||||
if (artiProxy != null) {
|
// (e.g., by transformLatest switching modes), we don't leak a
|
||||||
// Already running — just ensure status is up to date
|
// half-started ArtiProxy with no reference to stop it.
|
||||||
if (bootstrapped.get()) {
|
withContext(NonCancellable) {
|
||||||
_status.value = TorServiceStatus.Active(currentPort)
|
mutex.withLock {
|
||||||
} else {
|
if (artiProxy != null) {
|
||||||
_status.value = TorServiceStatus.Connecting
|
// Already running — just ensure status is up to date
|
||||||
}
|
if (bootstrapped.get()) {
|
||||||
return
|
_status.value = TorServiceStatus.Active(currentPort)
|
||||||
}
|
} else {
|
||||||
|
_status.value = TorServiceStatus.Connecting
|
||||||
_status.value = TorServiceStatus.Connecting
|
|
||||||
bootstrapped.set(false)
|
|
||||||
lastLogTime.set(System.currentTimeMillis())
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
var socksPort = DEFAULT_SOCKS_PORT
|
|
||||||
var lastError: Exception? = null
|
|
||||||
|
|
||||||
for (attempt in 0 until MAX_PORT_RETRIES) {
|
|
||||||
try {
|
|
||||||
val proxy =
|
|
||||||
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++
|
|
||||||
}
|
}
|
||||||
|
return@withContext
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastError != null) {
|
_status.value = TorServiceStatus.Connecting
|
||||||
Log.e("TorService") { "Failed to start Arti after $MAX_PORT_RETRIES attempts" }
|
bootstrapped.set(false)
|
||||||
_status.value = TorServiceStatus.Off
|
lastLogTime.set(System.currentTimeMillis())
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
var socksPort = DEFAULT_SOCKS_PORT
|
||||||
|
var lastError: Exception? = null
|
||||||
|
|
||||||
|
for (attempt in 0 until MAX_PORT_RETRIES) {
|
||||||
|
try {
|
||||||
|
val proxy =
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun stop() {
|
suspend fun stop() {
|
||||||
mutex.withLock {
|
// NonCancellable ensures stop() completes fully even if the caller
|
||||||
val proxy = artiProxy ?: return@withLock
|
// is cancelled, preventing leaked ArtiProxy instances and file locks.
|
||||||
artiProxy = null
|
withContext(NonCancellable) {
|
||||||
bootstrapped.set(false)
|
mutex.withLock {
|
||||||
|
val proxy = artiProxy ?: return@withContext
|
||||||
|
artiProxy = null
|
||||||
|
bootstrapped.set(false)
|
||||||
|
|
||||||
Log.d("TorService", "Stopping Arti")
|
Log.d("TorService", "Stopping Arti")
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
// Give the native layer time to release file locks
|
_status.value = TorServiceStatus.Off
|
||||||
delay(STOP_SETTLE_MS)
|
|
||||||
}
|
}
|
||||||
_status.value = TorServiceStatus.Off
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user