diff --git a/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/GatewayService.kt b/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/GatewayService.kt index c290859..785ef4a 100644 --- a/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/GatewayService.kt +++ b/flutter_app/android/app/src/main/kotlin/com/nxg/openclawproot/GatewayService.kt @@ -274,6 +274,7 @@ class GatewayService : Service() { } private fun stopGateway() { + val procToStop: Process? synchronized(lock) { stopping = true restartCount = maxRestarts // Prevent auto-restart @@ -281,14 +282,31 @@ class GatewayService : Service() { uptimeThread = null watchdogThread?.interrupt() watchdogThread = null - gatewayProcess?.let { - try { - it.destroyForcibly() - } catch (_: Exception) {} - gatewayProcess = null - } + // Interrupt the gateway thread in case it is sleeping during an + // auto-restart delay so it wakes up and sees stopping=true. + gatewayThread?.interrupt() + gatewayThread = null + procToStop = gatewayProcess + gatewayProcess = null } emitLog("Gateway stopped by user") + // Gracefully terminate proot via SIGTERM first, allowing its --kill-on-exit + // handler to kill child processes (node.js / openclaw daemon) before proot + // exits. destroyForcibly() (SIGKILL) bypasses proot's exit handler, which + // can leave the gateway daemon alive even after proot is killed. + procToStop?.let { proc -> + Thread({ + try { + proc.destroy() // SIGTERM — lets proot clean up its children + if (!proc.waitFor(3, java.util.concurrent.TimeUnit.SECONDS)) { + // proot did not exit cleanly; force-kill it. + proc.destroyForcibly() + } + } catch (_: Exception) { + try { proc.destroyForcibly() } catch (_: Exception) {} + } + }, "gateway-stop").apply { isDaemon = true }.start() + } } /** Watchdog: periodically checks if the proot process is alive. diff --git a/flutter_app/lib/screens/settings_screen.dart b/flutter_app/lib/screens/settings_screen.dart index 3aad664..2c67454 100644 --- a/flutter_app/lib/screens/settings_screen.dart +++ b/flutter_app/lib/screens/settings_screen.dart @@ -120,11 +120,11 @@ class _SettingsScreenState extends State { title: const Text('Setup Storage'), subtitle: Text(_storageGranted ? 'Granted — proot can access /sdcard. Revoke if not needed.' - : 'Allow access to shared storage'), + : 'Not granted (recommended) — tap to grant only if needed'), leading: const Icon(Icons.sd_storage), trailing: _storageGranted ? const Icon(Icons.warning_amber, color: AppColors.statusAmber) - : const Icon(Icons.warning, color: AppColors.statusAmber), + : const Icon(Icons.check_circle, color: AppColors.statusGreen), onTap: () async { await NativeBridge.requestStoragePermission(); // Refresh after returning from permission screen