fix(launcher): refuse to spawn when a CODESYS.exe is already running
The launcher only knew about its own state machine -- so any time a
prior CODESYS.exe was still alive (orphan from a crashed MCP session,
the user's own interactive IDE, or a CODESYS still mid-shutdown),
calling launch() happily spawned a SECOND CODESYS. The two instances
then raced on the project file lock and the loser surfaced the
"project is currently in use by <user> on <host>" modal, which blocks
the IDE thread and freezes every subsequent script call -- the agent
keeps timing out at 60s with no useful diagnostic.
Hit twice in a single session on 2026-04-25 during git_* smoke tests:
the user pointed at the running taskbar twice ("you are opening
shitloads of codesys sessions" / "you keep opening TWO codesyses --
then this message comes up and halts all your progress").
Adds findRunningCodesysPids() -- a tasklist-based scan -- and a
pre-spawn guard at the top of launch() that returns a clear,
action-oriented error listing the offending PIDs and the two
remediations (close the IDE manually, or call shutdown_codesys if the
launcher owns the existing process). No-op on non-Windows.
Doesn't try to adopt the existing process: that would require sharing
its IPC dir + watcher state, which the launcher cannot recover after
the fact. Refusing the spawn is the only safe response.
This commit is contained in:
parent
e6cfa5730b
commit
95a884bf1e
1 changed files with 58 additions and 1 deletions
|
|
@ -6,7 +6,7 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { spawn, execSync, ChildProcess } from 'child_process';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { LauncherConfig, LauncherStatus, CodesysState, IpcResult, ScriptExecutor } from './types';
|
||||
import { IpcClient, DEFAULT_IPC_CONFIG } from './ipc';
|
||||
|
|
@ -14,6 +14,40 @@ import { ScriptManager } from './script-manager';
|
|||
import { launcherLog } from './logger';
|
||||
|
||||
const SESSION_DIR_PREFIX = 'codesys-mcp-persistent';
|
||||
|
||||
/**
|
||||
* Returns the PIDs of every CODESYS.exe currently running on this Windows
|
||||
* machine -- whether spawned by this launcher, by a previous MCP session that
|
||||
* crashed without cleanup, or by the user opening CODESYS interactively.
|
||||
*
|
||||
* Used as a pre-launch guard so the launcher never spawns a second CODESYS
|
||||
* alongside an existing one. Two CODESYS instances against the same project
|
||||
* file race on the lock and the loser pops a "project is currently in use"
|
||||
* modal that freezes the IDE thread, breaking every subsequent script call
|
||||
* with 60s timeouts. The cheapest fix is to refuse the duplicate spawn.
|
||||
*
|
||||
* Returns an empty list on non-Windows or if tasklist fails (we treat that
|
||||
* as "can't tell" rather than blocking; the user always retains the option
|
||||
* to close manually).
|
||||
*/
|
||||
function findRunningCodesysPids(): number[] {
|
||||
if (process.platform !== 'win32') return [];
|
||||
try {
|
||||
const out = execSync(
|
||||
'tasklist /FI "IMAGENAME eq CODESYS.exe" /FO CSV /NH',
|
||||
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }
|
||||
);
|
||||
const pids: number[] = [];
|
||||
for (const line of out.split(/\r?\n/)) {
|
||||
// CSV with quotes: "CODESYS.exe","12345","Console","1","456,789 K"
|
||||
const m = line.match(/^"CODESYS\.exe","(\d+)"/);
|
||||
if (m) pids.push(Number(m[1]));
|
||||
}
|
||||
return pids;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const READY_TIMEOUT_MS = 60_000;
|
||||
const READY_POLL_MS = 500;
|
||||
const SHUTDOWN_WAIT_MS = 5_000;
|
||||
|
|
@ -51,6 +85,29 @@ export class CodesysLauncher implements ScriptExecutor {
|
|||
throw new Error(err);
|
||||
}
|
||||
|
||||
// Refuse to spawn alongside an existing CODESYS.exe. Two instances against
|
||||
// the same project file race on the lock and the loser pops a modal that
|
||||
// freezes script execution. This catches:
|
||||
// - orphans from a prior MCP session that crashed (exit code 0) without
|
||||
// taking the IDE down with it
|
||||
// - the user's own interactive CODESYS instance
|
||||
// - a CODESYS still mid-shutdown after a previous shutdown_codesys call
|
||||
const existingPids = findRunningCodesysPids();
|
||||
if (existingPids.length > 0) {
|
||||
const msg =
|
||||
`Refusing to launch: ${existingPids.length} CODESYS.exe process(es) ` +
|
||||
`already running (PID(s): ${existingPids.join(', ')}). The MCP launcher ` +
|
||||
`cannot share IPC with an instance it did not spawn, and a second ` +
|
||||
`CODESYS racing on the same project triggers a "project is currently ` +
|
||||
`in use" modal that blocks all script execution. Close the existing ` +
|
||||
`CODESYS window(s) first, or call shutdown_codesys if this server owns ` +
|
||||
`the running instance, then retry.`;
|
||||
launcherLog.warn(msg);
|
||||
this.lastError = msg;
|
||||
this.setState('error');
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
this.setState('launching');
|
||||
this.sessionId = uuidv4();
|
||||
this.ipcDir = path.join(os.tmpdir(), SESSION_DIR_PREFIX, this.sessionId);
|
||||
|
|
|
|||
Loading…
Reference in a new issue