From 9fb569bdb057e5db63b4dbf2d81f704c16a37f8b Mon Sep 17 00:00:00 2001 From: Karstein Phobic Nyvold Kvistad Date: Sun, 26 Apr 2026 01:20:39 +0200 Subject: [PATCH] shutdown_codesys: kill orphan CODESYS.exe when launcher has no tracked PID Real bug surfaced after a VSC restart left a CODESYS.exe alive that the new MCP server didn't spawn (state=stopped/error, this.pid=null). The previous shutdown() early-returned at the top -- the launcher was 'stopped' so it considered itself done -- which: 1. Left the orphan CODESYS.exe alive (couldn't run the project). 2. Made the refuse-on-duplicate guard (95a884b) block every subsequent launch_codesys with 'CODESYS already running'. The launcher was effectively bricked: shutdown said success but did nothing, launch refused. Hit during today's release-pipeline test loop. Fix: when shutdown() is called with state=stopped/error AND this.pid is null AND findRunningCodesysPids() returns non-empty, taskkill the orphans before the early-return. Graceful WM_CLOSE first, then 2s grace, then -F force-kill anything still alive. Doesn't change the happy-path (launcher tracks its own PID, state ready -> stopping -> stopped) -- that flow is untouched. The new code only runs when the launcher would otherwise have been a no-op on something the OS still has active. --- src/launcher.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/launcher.ts b/src/launcher.ts index 52f7730..afe93ac 100644 --- a/src/launcher.ts +++ b/src/launcher.ts @@ -188,7 +188,35 @@ export class CodesysLauncher implements ScriptExecutor { /** Graceful shutdown */ async shutdown(): Promise { - if (this.state === 'stopped' || this.state === 'stopping') return; + // Orphan-killing fallback: if the launcher itself has no tracked PID + // (state stopped/error after a fresh MCP server start) but a CODESYS.exe + // is alive on the box from a previous session, the previous early-return + // would say "shutdown_codesys success" and do nothing. This left the + // launcher's refuse-on-duplicate guard permanently blocking new spawns. + // Now we taskkill any orphans we can find before the early-return so the + // tool actually does something useful in this state. + if (this.state === 'stopped' || this.state === 'stopping') { + if (this.pid === null) { + const orphans = findRunningCodesysPids(); + if (orphans.length > 0) { + launcherLog.info(`shutdown_codesys: launcher has no tracked PID but found ${orphans.length} orphan CODESYS.exe (PIDs: ${orphans.join(', ')}). Force-killing.`); + for (const pid of orphans) { + try { + execSync(`taskkill /PID ${pid}`, { timeout: 5000, stdio: 'ignore' }); + } catch { /* ignore graceful failures, force-kill below */ } + } + // Give them a moment to close gracefully + await this.sleep(2_000); + const stillAlive = findRunningCodesysPids(); + for (const pid of stillAlive) { + try { + execSync(`taskkill /F /PID ${pid}`, { timeout: 5000, stdio: 'ignore' }); + } catch { /* nothing else to try */ } + } + } + } + return; + } this.setState('stopping'); this.stopHealthMonitor();