0
0
Fork 0

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.
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-04-26 01:20:39 +02:00
parent a570132c9c
commit 9fb569bdb0

View file

@ -188,7 +188,35 @@ export class CodesysLauncher implements ScriptExecutor {
/** Graceful shutdown */
async shutdown(): Promise<void> {
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();