mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Refactor ShellTool: remove ping hack and timeout, optimize cleanup
This commit is contained in:
@@ -123,15 +123,25 @@ export class ShellExecutionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup child processes
|
// Cleanup child processes
|
||||||
for (const pid of this.activeChildProcesses) {
|
if (os.platform() === 'win32') {
|
||||||
try {
|
if (this.activeChildProcesses.size > 0) {
|
||||||
if (os.platform() === 'win32') {
|
try {
|
||||||
spawnSync('taskkill', ['/pid', pid.toString(), '/f', '/t']);
|
const args = ['/f', '/t'];
|
||||||
} else {
|
for (const pid of this.activeChildProcesses) {
|
||||||
process.kill(-pid, 'SIGKILL');
|
args.push('/pid', pid.toString());
|
||||||
|
}
|
||||||
|
spawnSync('taskkill', args);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const pid of this.activeChildProcesses) {
|
||||||
|
try {
|
||||||
|
process.kill(-pid, 'SIGKILL');
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -833,12 +833,12 @@ describe('ShellTool', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Windows background execution', () => {
|
describe('Windows background execution', () => {
|
||||||
it('should append keep-alive ping with && on Windows for background tasks', async () => {
|
it('should clean up trailing ampersand on Windows for background tasks', async () => {
|
||||||
vi.mocked(os.platform).mockReturnValue('win32');
|
vi.mocked(os.platform).mockReturnValue('win32');
|
||||||
const mockAbortSignal = new AbortController().signal;
|
const mockAbortSignal = new AbortController().signal;
|
||||||
|
|
||||||
const invocation = shellTool.build({
|
const invocation = shellTool.build({
|
||||||
command: 'npm start',
|
command: 'npm start &',
|
||||||
is_background: true,
|
is_background: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -859,7 +859,7 @@ describe('ShellTool', () => {
|
|||||||
await promise;
|
await promise;
|
||||||
|
|
||||||
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
expect(mockShellExecutionService).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('npm start && ping -n 86400 127.0.0.1 >nul'),
|
'npm start',
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export interface ShellToolParams {
|
|||||||
is_background: boolean;
|
is_background: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
directory?: string;
|
directory?: string;
|
||||||
timeout?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShellToolInvocation extends BaseToolInvocation<
|
export class ShellToolInvocation extends BaseToolInvocation<
|
||||||
@@ -138,14 +137,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
.toString('hex')}.tmp`;
|
.toString('hex')}.tmp`;
|
||||||
const tempFilePath = path.join(os.tmpdir(), tempFileName);
|
const tempFilePath = path.join(os.tmpdir(), tempFileName);
|
||||||
|
|
||||||
const timeoutMs = this.params.timeout ?? 3600000;
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const onAbort = () => abortController.abort();
|
|
||||||
signal.addEventListener('abort', onAbort);
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
abortController.abort();
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Add co-author to git commit commands
|
// Add co-author to git commit commands
|
||||||
const processedCommand = this.addCoAuthorToGitCommit(strippedCommand);
|
const processedCommand = this.addCoAuthorToGitCommit(strippedCommand);
|
||||||
@@ -165,16 +156,15 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
finalCommand = finalCommand.trim() + ' &';
|
finalCommand = finalCommand.trim() + ' &';
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Windows, append a keep-alive command to ensure the shell process
|
// On Windows, we rely on the race logic below to handle background tasks.
|
||||||
// stays alive even if the main command exits (e.g. spawns a detached child).
|
// We just ensure the command string is clean.
|
||||||
// This ensures we always have a valid PID for cleanup.
|
|
||||||
if (isWindows && shouldRunInBackground) {
|
if (isWindows && shouldRunInBackground) {
|
||||||
// Remove trailing & if present to avoid syntax errors (e.g. "cmd & & ping")
|
|
||||||
let cmd = finalCommand.trim();
|
let cmd = finalCommand.trim();
|
||||||
|
// Remove trailing & (common Linux habit, invalid on Windows at end of line)
|
||||||
while (cmd.endsWith('&')) {
|
while (cmd.endsWith('&')) {
|
||||||
cmd = cmd.slice(0, -1).trim();
|
cmd = cmd.slice(0, -1).trim();
|
||||||
}
|
}
|
||||||
finalCommand = cmd + ' && ping -n 86400 127.0.0.1 >nul';
|
finalCommand = cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pgrep is not available on Windows, so we can't get background PIDs
|
// pgrep is not available on Windows, so we can't get background PIDs
|
||||||
@@ -239,7 +229,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
lastUpdateTime = Date.now();
|
lastUpdateTime = Date.now();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
abortController.signal,
|
signal,
|
||||||
this.config.getShouldUseNodePtyShell(),
|
this.config.getShouldUseNodePtyShell(),
|
||||||
shellExecutionConfig ?? {},
|
shellExecutionConfig ?? {},
|
||||||
);
|
);
|
||||||
@@ -277,7 +267,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (errorPatterns.some((pattern) => outputStr.includes(pattern))) {
|
if (errorPatterns.some((pattern) => outputStr.includes(pattern))) {
|
||||||
abortController.abort();
|
|
||||||
return {
|
return {
|
||||||
llmContent: `Command failed to start: ${outputStr}`,
|
llmContent: `Command failed to start: ${outputStr}`,
|
||||||
returnDisplay: `Command failed to start: ${outputStr}`,
|
returnDisplay: `Command failed to start: ${outputStr}`,
|
||||||
@@ -407,8 +396,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||||||
...executionError,
|
...executionError,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
|
||||||
signal.removeEventListener('abort', onAbort);
|
|
||||||
if (fs.existsSync(tempFilePath)) {
|
if (fs.existsSync(tempFilePath)) {
|
||||||
fs.unlinkSync(tempFilePath);
|
fs.unlinkSync(tempFilePath);
|
||||||
}
|
}
|
||||||
@@ -542,11 +529,6 @@ export class ShellTool extends BaseDeclarativeTool<
|
|||||||
description:
|
description:
|
||||||
'(OPTIONAL) The absolute path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist.',
|
'(OPTIONAL) The absolute path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist.',
|
||||||
},
|
},
|
||||||
timeout: {
|
|
||||||
type: 'number',
|
|
||||||
description:
|
|
||||||
'(OPTIONAL) The timeout in milliseconds for the command. If not provided, a default timeout (1 hour) is applied.',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
required: ['command', 'is_background'],
|
required: ['command', 'is_background'],
|
||||||
},
|
},
|
||||||
@@ -571,9 +553,6 @@ export class ShellTool extends BaseDeclarativeTool<
|
|||||||
if (!params.command.trim()) {
|
if (!params.command.trim()) {
|
||||||
return 'Command cannot be empty.';
|
return 'Command cannot be empty.';
|
||||||
}
|
}
|
||||||
if (params.timeout !== undefined && params.timeout <= 0) {
|
|
||||||
return 'Timeout must be a positive number.';
|
|
||||||
}
|
|
||||||
if (getCommandRoots(params.command).length === 0) {
|
if (getCommandRoots(params.command).length === 0) {
|
||||||
return 'Could not identify command root to obtain permission from user.';
|
return 'Could not identify command root to obtain permission from user.';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user