fix(processes): fix multiple clawx processes running concurently (#589)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com> Co-authored-by: paisley <8197966+su8su@users.noreply.github.com> Co-authored-by: Felix <24791380+vcfgv@users.noreply.github.com>
This commit is contained in:
@@ -242,6 +242,7 @@ export class GatewayManager extends EventEmitter {
|
||||
await this.connect(port, externalToken);
|
||||
},
|
||||
onConnectedToExistingGateway: () => {
|
||||
|
||||
// If the existing gateway is actually our own spawned UtilityProcess
|
||||
// (e.g. after a self-restart code=1012), keep ownership so that
|
||||
// stop() can still terminate the process during a restart() cycle.
|
||||
@@ -250,6 +251,7 @@ export class GatewayManager extends EventEmitter {
|
||||
this.ownsProcess = false;
|
||||
this.setStatus({ pid: undefined });
|
||||
}
|
||||
|
||||
this.startHealthCheck();
|
||||
},
|
||||
waitForPortFree: async (port) => {
|
||||
@@ -356,6 +358,25 @@ export class GatewayManager extends EventEmitter {
|
||||
this.setStatus({ state: 'stopped', error: undefined, pid: undefined, connectedAt: undefined, uptime: undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort emergency cleanup for app-quit timeout paths.
|
||||
* Only terminates a process this manager still owns.
|
||||
*/
|
||||
async forceTerminateOwnedProcessForQuit(): Promise<boolean> {
|
||||
if (!this.process || !this.ownsProcess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const child = this.process;
|
||||
await terminateOwnedGatewayProcess(child);
|
||||
if (this.process === child) {
|
||||
this.process = null;
|
||||
}
|
||||
this.ownsProcess = false;
|
||||
this.setStatus({ pid: undefined });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart Gateway process
|
||||
*/
|
||||
@@ -724,6 +745,7 @@ export class GatewayManager extends EventEmitter {
|
||||
|
||||
this.process = child;
|
||||
this.ownsProcess = true;
|
||||
logger.debug(`Gateway manager now owns process pid=${child.pid ?? 'unknown'}`);
|
||||
this.lastSpawnSummary = lastSpawnSummary;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,39 +22,58 @@ export function warmupManagedPythonReadiness(): void {
|
||||
}
|
||||
|
||||
export async function terminateOwnedGatewayProcess(child: Electron.UtilityProcess): Promise<void> {
|
||||
let exited = false;
|
||||
const terminateWindowsProcessTree = async (pid: number): Promise<void> => {
|
||||
const cp = await import('child_process');
|
||||
await new Promise<void>((resolve) => {
|
||||
cp.exec(`taskkill /F /PID ${pid} /T`, { timeout: 5000, windowsHide: true }, () => resolve());
|
||||
});
|
||||
};
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
let exited = false;
|
||||
|
||||
// Register a single exit listener before any kill attempt to avoid
|
||||
// the race where exit fires between two separate `once('exit')` calls.
|
||||
child.once('exit', () => {
|
||||
exited = true;
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
|
||||
const pid = child.pid;
|
||||
logger.info(`Sending kill to Gateway process (pid=${pid ?? 'unknown'})`);
|
||||
try {
|
||||
child.kill();
|
||||
} catch {
|
||||
// ignore if already exited
|
||||
|
||||
if (process.platform === 'win32' && pid) {
|
||||
void terminateWindowsProcessTree(pid).catch((err) => {
|
||||
logger.warn(`Windows process-tree kill failed for Gateway pid=${pid}:`, err);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
child.kill();
|
||||
} catch {
|
||||
// ignore if already exited
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
if (!exited) {
|
||||
logger.warn(`Gateway did not exit in time, force-killing (pid=${pid ?? 'unknown'})`);
|
||||
if (pid) {
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
} catch {
|
||||
// ignore
|
||||
if (process.platform === 'win32') {
|
||||
void terminateWindowsProcessTree(pid).catch((err) => {
|
||||
logger.warn(`Forced Windows process-tree kill failed for Gateway pid=${pid}:`, err);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
}, 5000);
|
||||
|
||||
child.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,6 +245,9 @@ export async function findExistingGatewayProcess(options: {
|
||||
const pids = await getListeningProcessIds(port);
|
||||
if (pids.length > 0 && (!ownedPid || !pids.includes(String(ownedPid)))) {
|
||||
await terminateOrphanedProcessIds(port, pids);
|
||||
if (process.platform === 'win32') {
|
||||
await waitForPortFree(port, 10000);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user