Browse Source

Exit server when owner process dies (harness-agnostic cleanup)

start-server.sh passes $PPID as BRAINSTORM_OWNER_PID to the server.
The server checks every 60s if the owner process is still alive
(kill -0). If it's gone, the server shuts down immediately —
deletes .server-info, writes .server-stopped, exits cleanly.
Works across all harnesses (CC, Codex, Gemini CLI) since it
tracks the shell process that launched the script, which dies
when the harness dies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Jesse Vincent 6 months ago
parent
commit
ec99b7c4a4
2 changed files with 27 additions and 18 deletions
  1. 25 16
      skills/brainstorming/scripts/server.js
  2. 2 2
      skills/brainstorming/scripts/start-server.sh

+ 25 - 16
skills/brainstorming/scripts/server.js

@@ -77,6 +77,7 @@ const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() *
 const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
 const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
 const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
+const OWNER_PID = process.env.BRAINSTORM_OWNER_PID ? Number(process.env.BRAINSTORM_OWNER_PID) : null;
 
 const MIME_TYPES = {
   '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
@@ -294,22 +295,30 @@ function startServer() {
   });
   watcher.on('error', (err) => console.error('fs.watch error:', err.message));
 
-  // Exit if no activity for 30 minutes (prevents orphaned servers)
-  const idleCheck = setInterval(() => {
-    if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) {
-      console.log(JSON.stringify({ type: 'server-stopped', reason: 'idle timeout' }));
-      const infoFile = path.join(SCREEN_DIR, '.server-info');
-      if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
-      fs.writeFileSync(
-        path.join(SCREEN_DIR, '.server-stopped'),
-        JSON.stringify({ reason: 'idle timeout', timestamp: Date.now() }) + '\n'
-      );
-      watcher.close();
-      clearInterval(idleCheck);
-      server.close(() => process.exit(0));
-    }
-  }, 60 * 1000); // check every minute
-  idleCheck.unref(); // don't keep process alive just for the timer
+  function shutdown(reason) {
+    console.log(JSON.stringify({ type: 'server-stopped', reason }));
+    const infoFile = path.join(SCREEN_DIR, '.server-info');
+    if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
+    fs.writeFileSync(
+      path.join(SCREEN_DIR, '.server-stopped'),
+      JSON.stringify({ reason, timestamp: Date.now() }) + '\n'
+    );
+    watcher.close();
+    clearInterval(lifecycleCheck);
+    server.close(() => process.exit(0));
+  }
+
+  function ownerAlive() {
+    if (!OWNER_PID) return true;
+    try { process.kill(OWNER_PID, 0); return true; } catch (e) { return false; }
+  }
+
+  // Check every 60s: exit if owner process died or idle for 30 minutes
+  const lifecycleCheck = setInterval(() => {
+    if (!ownerAlive()) shutdown('owner process exited');
+    else if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown('idle timeout');
+  }, 60 * 1000);
+  lifecycleCheck.unref();
 
   server.listen(PORT, HOST, () => {
     const info = JSON.stringify({

+ 2 - 2
skills/brainstorming/scripts/start-server.sh

@@ -91,13 +91,13 @@ cd "$SCRIPT_DIR"
 # Foreground mode for environments that reap detached/background processes.
 if [[ "$FOREGROUND" == "true" ]]; then
   echo "$$" > "$PID_FILE"
-  env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js
+  env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$PPID" node server.js
   exit $?
 fi
 
 # Start server, capturing output to log file
 # Use nohup to survive shell exit; disown to remove from job table
-nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" node server.js > "$LOG_FILE" 2>&1 &
+nohup env BRAINSTORM_DIR="$SCREEN_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$PPID" node server.js > "$LOG_FILE" 2>&1 &
 SERVER_PID=$!
 disown "$SERVER_PID" 2>/dev/null
 echo "$SERVER_PID" > "$PID_FILE"