mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
fix: handle windows background execution errors and add tests
This commit is contained in:
@@ -108,47 +108,38 @@ export class ShellExecutionService {
|
|||||||
private static activePtys = new Map<number, ActivePty>();
|
private static activePtys = new Map<number, ActivePty>();
|
||||||
private static activeChildProcesses = new Set<number>();
|
private static activeChildProcesses = new Set<number>();
|
||||||
|
|
||||||
static {
|
static cleanup() {
|
||||||
const cleanup = () => {
|
// Cleanup PTYs
|
||||||
// Cleanup PTYs
|
for (const [pid, pty] of this.activePtys) {
|
||||||
for (const [pid, pty] of this.activePtys) {
|
try {
|
||||||
try {
|
if (os.platform() === 'win32') {
|
||||||
if (os.platform() === 'win32') {
|
pty.ptyProcess.kill();
|
||||||
pty.ptyProcess.kill();
|
} else {
|
||||||
} else {
|
process.kill(-pid, 'SIGKILL');
|
||||||
process.kill(-pid, 'SIGKILL');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup child processes
|
|
||||||
for (const pid of this.activeChildProcesses) {
|
|
||||||
try {
|
|
||||||
if (os.platform() === 'win32') {
|
|
||||||
spawnSync('taskkill', ['/pid', pid.toString(), '/f', '/t']);
|
|
||||||
} else {
|
|
||||||
process.kill(-pid, 'SIGKILL');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
process.on('exit', cleanup);
|
|
||||||
|
|
||||||
// Ensure cleanup happens on SIGINT/SIGTERM
|
|
||||||
const signalHandler = () => {
|
|
||||||
process.exit();
|
|
||||||
};
|
|
||||||
|
|
||||||
// We only attach these if we are in a node environment where we can control the process
|
|
||||||
if (typeof process !== 'undefined' && process.on) {
|
|
||||||
process.on('SIGINT', signalHandler);
|
|
||||||
process.on('SIGTERM', signalHandler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup child processes
|
||||||
|
for (const pid of this.activeChildProcesses) {
|
||||||
|
try {
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
spawnSync('taskkill', ['/pid', pid.toString(), '/f', '/t']);
|
||||||
|
} else {
|
||||||
|
process.kill(-pid, 'SIGKILL');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
process.on('exit', () => {
|
||||||
|
ShellExecutionService.cleanup();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -831,4 +831,33 @@ describe('ShellTool', () => {
|
|||||||
expect(shellTool.description).toMatchSnapshot();
|
expect(shellTool.description).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Windows background execution', () => {
|
||||||
|
it('should detect immediate failure in Windows background task', async () => {
|
||||||
|
vi.mocked(os.platform).mockReturnValue('win32');
|
||||||
|
const mockAbortSignal = new AbortController().signal;
|
||||||
|
|
||||||
|
const invocation = shellTool.build({
|
||||||
|
command: 'invalid_command',
|
||||||
|
is_background: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const promise = invocation.execute(mockAbortSignal);
|
||||||
|
|
||||||
|
// Wait a tick to ensure mockShellOutputCallback is assigned
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
if (mockShellOutputCallback) {
|
||||||
|
mockShellOutputCallback({
|
||||||
|
type: 'data',
|
||||||
|
chunk:
|
||||||
|
"'invalid_command' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await promise;
|
||||||
|
expect(result.error).toBeDefined();
|
||||||
|
expect(result.llmContent).toContain('Command failed to start');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -194,16 +194,16 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
commandToExecute,
|
commandToExecute,
|
||||||
cwd,
|
cwd,
|
||||||
(event: ShellOutputEvent) => {
|
(event: ShellOutputEvent) => {
|
||||||
if (!updateOutput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let shouldUpdate = false;
|
let shouldUpdate = false;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'data':
|
case 'data':
|
||||||
if (isBinaryStream) break;
|
if (isBinaryStream) break;
|
||||||
cumulativeOutput = event.chunk;
|
if (typeof cumulativeOutput === 'string') {
|
||||||
|
cumulativeOutput += event.chunk;
|
||||||
|
} else {
|
||||||
|
cumulativeOutput = event.chunk;
|
||||||
|
}
|
||||||
shouldUpdate = true;
|
shouldUpdate = true;
|
||||||
break;
|
break;
|
||||||
case 'binary_detected':
|
case 'binary_detected':
|
||||||
@@ -226,7 +226,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate && updateOutput) {
|
||||||
updateOutput(
|
updateOutput(
|
||||||
typeof cumulativeOutput === 'string'
|
typeof cumulativeOutput === 'string'
|
||||||
? cumulativeOutput
|
? cumulativeOutput
|
||||||
@@ -258,6 +258,32 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
if (raceResult === null) {
|
if (raceResult === null) {
|
||||||
// Timeout reached, process is still running.
|
// Timeout reached, process is still running.
|
||||||
|
// throw new Error(`DEBUG: raceResult is null. Output: ${JSON.stringify(cumulativeOutput)}`);
|
||||||
|
|
||||||
|
// Check for common Windows error messages in the output
|
||||||
|
const outputStr =
|
||||||
|
typeof cumulativeOutput === 'string'
|
||||||
|
? cumulativeOutput
|
||||||
|
: JSON.stringify(cumulativeOutput);
|
||||||
|
console.log('DEBUG: outputStr:', outputStr);
|
||||||
|
const errorPatterns = [
|
||||||
|
'is not recognized as an internal or external command',
|
||||||
|
'The system cannot find the path specified',
|
||||||
|
'Access is denied',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (errorPatterns.some((pattern) => outputStr.includes(pattern))) {
|
||||||
|
abortController.abort();
|
||||||
|
return {
|
||||||
|
llmContent: `Command failed to start: ${outputStr}`,
|
||||||
|
returnDisplay: `Command failed to start: ${outputStr}`,
|
||||||
|
error: {
|
||||||
|
type: ToolErrorType.EXECUTION_FAILED,
|
||||||
|
message: `Command failed to start: ${outputStr}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const pidMsg = pid ? ` PID: ${pid}` : '';
|
const pidMsg = pid ? ` PID: ${pid}` : '';
|
||||||
const winHint = isWindows
|
const winHint = isWindows
|
||||||
? ' (Note: Use taskkill /F /T /PID <pid> to stop)'
|
? ' (Note: Use taskkill /F /T /PID <pid> to stop)'
|
||||||
|
|||||||
Reference in New Issue
Block a user