fix startup issues

This commit is contained in:
Fabian Freund
2026-01-08 10:01:19 +01:00
parent 6f4654e0be
commit a64a5672dd
5 changed files with 129 additions and 51 deletions
@@ -47,7 +47,13 @@ class TorProxyScreen extends HookConsumerWidget {
if (next.requireValue.isRunning != previous?.value?.isRunning || if (next.requireValue.isRunning != previous?.value?.isRunning ||
next.requireValue.bootstrapProgress != next.requireValue.bootstrapProgress !=
previous?.value?.bootstrapProgress) { previous?.value?.bootstrapProgress) {
torPendingRequest.value = null; if (torPendingRequest.value == true) {
if (next.requireValue.bootstrapProgress > 0) {
torPendingRequest.value = null;
}
} else {
torPendingRequest.value = null;
}
} }
} }
}); });
@@ -58,9 +64,16 @@ class TorProxyScreen extends HookConsumerWidget {
), ),
); );
final torIsBootstrapped = ref.watch(
torProxyServiceProvider.select(
(value) => value.value?.bootstrapProgress == 100,
),
);
final torIsBusy = final torIsBusy =
torPendingRequest.value != null || torPendingRequest.value != null ||
bootstrapProgress > 0 && bootstrapProgress < 100; bootstrapProgress > 0 && bootstrapProgress < 100;
final torSettings = ref.watch(torSettingsWithDefaultsProvider); final torSettings = ref.watch(torSettingsWithDefaultsProvider);
useOnInitialization(() async { useOnInitialization(() async {
@@ -446,7 +459,7 @@ class TorProxyScreen extends HookConsumerWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (torIsBusy) if (torPendingRequest.value != false && torIsBusy)
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -463,7 +476,9 @@ class TorProxyScreen extends HookConsumerWidget {
), ),
], ],
) )
else if (torIsRunning) else if (torPendingRequest.value != false &&
torIsRunning &&
torIsBootstrapped)
Padding( Padding(
padding: const EdgeInsets.only(top: 24.0), padding: const EdgeInsets.only(top: 24.0),
child: Row( child: Row(
@@ -23,7 +23,9 @@ class FlutterTorPlugin : FlutterPlugin, TorApi {
companion object { companion object {
private const val TAG = "FlutterTorPlugin" private const val TAG = "FlutterTorPlugin"
private const val SERVICE_CONNECTION_TIMEOUT_MS = 10000L // Increased timeout to 60 seconds to account for native TorService binding
// which can take 30+ seconds during initial setup
private const val SERVICE_CONNECTION_TIMEOUT_MS = 60000L
} }
private var context: Context? = null private var context: Context? = null
@@ -47,8 +47,6 @@ class LogStreamHandler(messenger: BinaryMessenger) {
* @param status Current Tor status * @param status Current Tor status
*/ */
fun sendStatusChange(status: TorStatus) { fun sendStatusChange(status: TorStatus) {
Log.d(TorManager.Companion.TAG, "sendStatusChange() returning: isRunning=${status.isRunning}, socksPort=${status.socksPort}, bootstrap=${status.bootstrapProgress}")
mainHandler.post { mainHandler.post {
try { try {
torLogApi.onStatusChanged(status) { } torLogApi.onStatusChanged(status) { }
@@ -134,10 +134,10 @@ class TorConfig(private val config: TorConfiguration) {
append("DormantClientTimeout 10 minutes\n") append("DormantClientTimeout 10 minutes\n")
append("DormantCanceledByStartup 1\n") append("DormantCanceledByStartup 1\n")
// Note: DisableNetwork is set to 1 in defaults.torrc // CRITICAL: Set DisableNetwork 1 to prevent bootstrap before event listener is ready
// It will be enabled via control port after setup completes (matching Orbot) // This will be changed to 0 via control port AFTER we set up event listeners
// We DON'T set it here to avoid overriding the defaults.torrc setting // This matches Orbot's approach and ensures we receive all bootstrap events
append("DisableNetwork 0\n") append("DisableNetwork 1\n")
append("Log notice stdout\n") // Log to stdout for capture append("Log notice stdout\n") // Log to stdout for capture
append("\n") append("\n")
@@ -40,13 +40,21 @@ class TorManager(
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Store event listener as a field to prevent garbage collection
private var torEventListener: TorEventListener? = null
var socksPort: Int = -1 var socksPort: Int = -1
private set private set
// Note: No controlPort - tor-android uses ControlSocket (Unix domain socket) instead // Note: No controlPort - tor-android uses ControlSocket (Unix domain socket) instead
// This is more secure than TCP ControlPort as it uses file permissions for access control // This is more secure than TCP ControlPort as it uses file permissions for access control
@Volatile
private var isRunning = false private var isRunning = false
@Volatile
private var bootstrapProgress = 0 private var bootstrapProgress = 0
// Lock for synchronizing status updates
private val statusLock = Any()
/** /**
* Start Tor with the given configuration * Start Tor with the given configuration
* @param config Tor configuration from Flutter * @param config Tor configuration from Flutter
@@ -119,12 +127,10 @@ class TorManager(
// Start TorService // Start TorService
// Note: torrcFile is now written to the correct location via TorService.getTorrc() // Note: torrcFile is now written to the correct location via TorService.getTorrc()
// so TorService will automatically find and use it // so TorService will automatically find and use it
// Note: isRunning will be set to true in setupControlConnection() before network is enabled
// This ensures status is consistent when bootstrap events start arriving
startTorService() startTorService()
isRunning = true
logHandler.notice("Tor started successfully")
sendStatusUpdate()
socksPort socksPort
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to start Tor", e) Log.e(TAG, "Failed to start Tor", e)
@@ -190,7 +196,7 @@ class TorManager(
controlConnection = conn controlConnection = conn
setupControlConnection(conn) setupControlConnection(conn)
if (continuation.isActive) { if (continuation.isActive) {
continuation.resume(Unit) {} continuation.resume(Unit)
} }
} else { } else {
val error = Exception("Failed to get control connection after 30 seconds") val error = Exception("Failed to get control connection after 30 seconds")
@@ -205,6 +211,7 @@ class TorManager(
Log.w(TAG, "TorService disconnected") Log.w(TAG, "TorService disconnected")
torService = null torService = null
controlConnection = null controlConnection = null
torEventListener = null
} }
} }
@@ -225,11 +232,23 @@ class TorManager(
/** /**
* Setup control connection and event listeners * Setup control connection and event listeners
* This follows Orbot's approach: query ports first, then set up events, then enable network
*/ */
private fun setupControlConnection(conn: TorControlConnection) { private fun setupControlConnection(conn: TorControlConnection) {
try { try {
// Add event listener // Query control connection to verify SOCKS port (like Orbot's initControlConnection)
conn.addRawEventListener(TorEventListener()) // This also properly initializes the control connection for event delivery
try {
conn.getInfo("net/listeners/socks")
} catch (e: Exception) {
Log.w(TAG, "Could not query SOCKS port from control connection", e)
}
logHandler.notice("Connected to Tor control port")
// Create and store event listener instance (prevents garbage collection)
torEventListener = TorEventListener()
conn.addRawEventListener(torEventListener)
// Subscribe to events (matching Orbot's event subscriptions) // Subscribe to events (matching Orbot's event subscriptions)
conn.setEvents(listOf( conn.setEvents(listOf(
@@ -243,14 +262,28 @@ class TorManager(
TorControlCommands.EVENT_ADDRMAP TorControlCommands.EVENT_ADDRMAP
)) ))
// Enable network now that configuration is complete (like Orbot does) Log.d(TAG, "Control connection setup complete, enabling network")
conn.setConf("DisableNetwork", "0")
Log.d(TAG, "Control connection setup complete") // Set isRunning=true BEFORE enabling network so status is consistent
logHandler.notice("Connected to Tor control port") // when bootstrap events start arriving
synchronized(statusLock) {
isRunning = true
}
logHandler.notice("Tor started successfully")
sendStatusUpdate()
// Enable network now that configuration is complete (like Orbot does)
// This will trigger bootstrap events to start
conn.setConf("DisableNetwork", "0")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to setup control connection", e) Log.e(TAG, "Failed to setup control connection", e)
logHandler.error("Control connection error: ${e.message}") logHandler.error("Control connection error: ${e.message}")
// Reset isRunning on failure
synchronized(statusLock) {
isRunning = false
}
torEventListener = null
throw e
} }
} }
@@ -262,15 +295,19 @@ class TorManager(
logHandler.notice("Stopping Tor...") logHandler.notice("Stopping Tor...")
try { try {
// Shutdown Tor gracefully // DON'T call shutdownTor() here - let the service's onDestroy() handle it
controlConnection?.shutdownTor("SHUTDOWN") // Otherwise we get a broken pipe error when service tries to shutdown again
delay(1000) // Give Tor time to shutdown
cleanup() cleanup()
// Give the native service time to fully stop before potential restart
// This is CRITICAL to prevent binding to a stale service instance
delay(2000)
logHandler.notice("Tor stopped") logHandler.notice("Tor stopped")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error stopping Tor", e) Log.e(TAG, "Error stopping Tor", e)
cleanup() cleanup()
delay(2000)
} }
} }
@@ -278,32 +315,57 @@ class TorManager(
* Cleanup resources * Cleanup resources
*/ */
private fun cleanup() { private fun cleanup() {
isRunning = false synchronized(statusLock) {
bootstrapProgress = 0 isRunning = false
socksPort = -1 bootstrapProgress = 0
socksPort = -1
}
try { try {
controlConnection?.let { // Unsubscribe from all events before removing listener
// Don't shutdown again, just close if (controlConnection != null) {
try {
controlConnection?.setEvents(emptyList())
} catch (e: Exception) {
// Connection may already be closed
}
} }
// Remove event listener
if (torEventListener != null && controlConnection != null) {
try {
controlConnection?.removeRawEventListener(torEventListener)
} catch (e: Exception) {
// Connection may already be closed
}
}
torEventListener = null
controlConnection = null controlConnection = null
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Error closing control connection", e) Log.w(TAG, "Error closing control connection", e)
} }
try { // Service cleanup
torServiceConnection?.let { runBlocking(Dispatchers.Main) {
context.unbindService(it) try {
torServiceConnection?.let {
context.unbindService(it)
}
torServiceConnection = null
// Small delay to ensure unbind completes before stopping service
delay(100)
// Stop the native TorService to ensure clean restart
val intent = Intent(context, org.torproject.jni.TorService::class.java)
context.stopService(intent)
} catch (e: Exception) {
Log.w(TAG, "Error unbinding TorService", e)
} }
torServiceConnection = null
} catch (e: Exception) {
Log.w(TAG, "Error unbinding TorService", e)
} }
torService = null torService = null
pluggableTransportManager.stopAll() pluggableTransportManager.stopAll()
sendStatusUpdate() sendStatusUpdate()
} }
@@ -326,18 +388,19 @@ class TorManager(
* Get current Tor status * Get current Tor status
*/ */
fun getStatus(): TorStatus { fun getStatus(): TorStatus {
val status = TorStatus( synchronized(statusLock) {
isRunning = isRunning, val status = TorStatus(
socksPort = if (isRunning) socksPort.toLong() else null, isRunning = isRunning,
bootstrapProgress = bootstrapProgress.toLong(), socksPort = if (isRunning) socksPort.toLong() else null,
currentCircuit = null, // TODO: track current circuit bootstrapProgress = bootstrapProgress.toLong(),
exitNodeCountry = null // TODO: track exit node country currentCircuit = null, // TODO: track current circuit
) exitNodeCountry = null // TODO: track exit node country
)
Log.d(TAG, "getStatus() returning: isRunning=$isRunning, socksPort=$socksPort, bootstrap=$bootstrapProgress") Log.d(TAG, "getStatus() returning: isRunning=$isRunning, socksPort=$socksPort, bootstrap=$bootstrapProgress")
// logHandler.sendStatusChange(status)
return status return status
}
} }
/** /**
@@ -352,13 +415,13 @@ class TorManager(
*/ */
private inner class TorEventListener : RawEventListener { private inner class TorEventListener : RawEventListener {
override fun onEvent(eventType: String, eventData: String) { override fun onEvent(eventType: String, eventData: String) {
Log.d(TAG, "Tor event: $eventType - $eventData")
// Handle bootstrap progress (comes in NOTICE events) // Handle bootstrap progress (comes in NOTICE events)
if (eventData.contains("Bootstrapped")) { if (eventData.contains("Bootstrapped")) {
val progress = extractBootstrapProgress(eventData) val progress = extractBootstrapProgress(eventData)
if (progress >= 0) { if (progress >= 0) {
bootstrapProgress = progress synchronized(statusLock) {
bootstrapProgress = progress
}
sendStatusUpdate() sendStatusUpdate()
if (progress == 100) { if (progress == 100) {