mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
fix(cli): catch sync exec throw in statusline to prevent crash (#3264) (#3310)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
child_process.exec() throws synchronously for spawn errors libuv does not report via the async 'error' event (EBADF, EINVAL, …). On macOS with Node 22, EBADF can surface during stdio pipe setup, and the uncaught throw escapes the debounce setTimeout callback and crashes the CLI. Wrap the exec call in try/catch so a failing statusline degrades to no output while the rest of the CLI keeps running.
This commit is contained in:
parent
d439e7d738
commit
6f29d24fb9
2 changed files with 80 additions and 15 deletions
|
|
@ -581,4 +581,57 @@ describe('useStatusLine', () => {
|
|||
expect(firstKill).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// --- Spawn failure handling (issue #3264) ---
|
||||
//
|
||||
// On macOS with Node 22, exec() can throw synchronously with EBADF when
|
||||
// stdio pipe setup fails. The throw must not escape doUpdate() — or the
|
||||
// setTimeout callback — or the whole CLI crashes.
|
||||
|
||||
describe('spawn failure handling', () => {
|
||||
it('does not crash when exec throws synchronously (EBADF)', () => {
|
||||
vi.mocked(child_process.exec).mockImplementationOnce((() => {
|
||||
const err = new Error('spawn EBADF') as NodeJS.ErrnoException;
|
||||
err.code = 'EBADF';
|
||||
throw err;
|
||||
}) as unknown as typeof child_process.exec);
|
||||
|
||||
setStatusLineConfig({ type: 'command', command: 'echo test' });
|
||||
|
||||
let result: { current: { text: string | null } } | undefined;
|
||||
expect(() => {
|
||||
result = renderHook(() => useStatusLine()).result;
|
||||
}).not.toThrow();
|
||||
expect(result!.current.text).toBeNull();
|
||||
});
|
||||
|
||||
it('recovers on subsequent state changes after a sync exec failure', async () => {
|
||||
// First call throws, subsequent calls succeed with the default mock.
|
||||
// Verifies activeChildRef and generationRef don't get wedged.
|
||||
vi.mocked(child_process.exec).mockImplementationOnce((() => {
|
||||
const err = new Error('spawn EBADF') as NodeJS.ErrnoException;
|
||||
err.code = 'EBADF';
|
||||
throw err;
|
||||
}) as unknown as typeof child_process.exec);
|
||||
|
||||
setStatusLineConfig({ type: 'command', command: 'echo test' });
|
||||
const { result, rerender } = renderHook(() => useStatusLine());
|
||||
|
||||
expect(result.current.text).toBeNull();
|
||||
expect(child_process.exec).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Trigger a re-execution via state change — should use the default mock.
|
||||
mockUIState.currentModel = 'new-model';
|
||||
rerender();
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(child_process.exec).toHaveBeenCalledTimes(2);
|
||||
await act(async () => {
|
||||
execCallback(null, 'recovered\n', '');
|
||||
});
|
||||
expect(result.current.text).toBe('recovered');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -269,21 +269,33 @@ export function useStatusLine(): {
|
|||
// Bump generation so earlier in-flight callbacks are ignored.
|
||||
const gen = ++generationRef.current;
|
||||
|
||||
const child = exec(
|
||||
cmd,
|
||||
{ cwd: cfg.getTargetDir(), timeout: 5000, maxBuffer: 1024 * 10 },
|
||||
(error, stdout) => {
|
||||
if (gen !== generationRef.current) return; // stale
|
||||
activeChildRef.current = undefined;
|
||||
if (!error && stdout) {
|
||||
// Strip only the trailing newline to preserve intentional whitespace.
|
||||
const line = stdout.replace(/\r?\n$/, '').split(/\r?\n/, 1)[0];
|
||||
setOutput(line || null);
|
||||
} else {
|
||||
setOutput(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
// exec() can throw synchronously: libuv reports a handful of spawn
|
||||
// errors (EACCES, ENOENT, …) via the async 'error' event, but anything
|
||||
// else — including EBADF, reported on macOS Node 22 in issue #3264 — is
|
||||
// thrown from ChildProcess.spawn. Without this guard the throw escapes
|
||||
// the setTimeout callback and crashes the CLI as uncaughtException.
|
||||
let child: ChildProcess;
|
||||
try {
|
||||
child = exec(
|
||||
cmd,
|
||||
{ cwd: cfg.getTargetDir(), timeout: 5000, maxBuffer: 1024 * 10 },
|
||||
(error, stdout) => {
|
||||
if (gen !== generationRef.current) return; // stale
|
||||
activeChildRef.current = undefined;
|
||||
if (!error && stdout) {
|
||||
// Strip only the trailing newline to preserve intentional whitespace.
|
||||
const line = stdout.replace(/\r?\n$/, '').split(/\r?\n/, 1)[0];
|
||||
setOutput(line || null);
|
||||
} else {
|
||||
setOutput(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
debugLog.error('statusline exec error:', (err as Error).message);
|
||||
setOutput(null);
|
||||
return;
|
||||
}
|
||||
|
||||
activeChildRef.current = child;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue