@@ -274,21 +274,39 @@ class GatewayService : Service() {
274274 }
275275
276276 private fun stopGateway () {
277+ val procToStop: Process ?
277278 synchronized(lock) {
278279 stopping = true
279280 restartCount = maxRestarts // Prevent auto-restart
280281 uptimeThread?.interrupt()
281282 uptimeThread = null
282283 watchdogThread?.interrupt()
283284 watchdogThread = null
284- gatewayProcess?. let {
285- try {
286- it.destroyForcibly ()
287- } catch (_ : Exception ) {}
288- gatewayProcess = null
289- }
285+ // Interrupt the gateway thread in case it is sleeping during an
286+ // auto-restart delay so it wakes up and sees stopping=true.
287+ gatewayThread?.interrupt ()
288+ gatewayThread = null
289+ procToStop = gatewayProcess
290+ gatewayProcess = null
290291 }
291292 emitLog(" Gateway stopped by user" )
293+ // Gracefully terminate proot via SIGTERM first, allowing its --kill-on-exit
294+ // handler to kill child processes (node.js / openclaw daemon) before proot
295+ // exits. destroyForcibly() (SIGKILL) bypasses proot's exit handler, which
296+ // can leave the gateway daemon alive even after proot is killed.
297+ procToStop?.let { proc ->
298+ Thread ({
299+ try {
300+ proc.destroy() // SIGTERM — lets proot clean up its children
301+ if (! proc.waitFor(3 , java.util.concurrent.TimeUnit .SECONDS )) {
302+ // proot did not exit cleanly; force-kill it.
303+ proc.destroyForcibly()
304+ }
305+ } catch (_: Exception ) {
306+ try { proc.destroyForcibly() } catch (_: Exception ) {}
307+ }
308+ }, " gateway-stop" ).apply { isDaemon = true }.start()
309+ }
292310 }
293311
294312 /* * Watchdog: periodically checks if the proot process is alive.
0 commit comments