mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(shell): avoid self-kill from pgrep selectors (#6544)
* fix(shell): avoid self-kill from pgrep selectors Fixes #6246 * fix(shell): handle pgrep review cases
This commit is contained in:
parent
0a54652e07
commit
10fa9effbb
4 changed files with 201 additions and 5 deletions
|
|
@ -52,6 +52,7 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
|
|||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
- Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.
|
||||
- To stop a background command started by this tool, use \`task_stop\` when a task id is available. Do not use broad process-name kills such as \`kill $(pgrep node)\`, \`pkill node\`, or \`killall node\`; use a specific PID or process group id where supported.
|
||||
- Use foreground execution (is_background: false) for:
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
|
|
@ -106,6 +107,7 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
|
|||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
|
||||
- To stop a background command started by this tool, use \`task_stop\` when a task id is available. Do not use broad process-name kills such as \`kill $(pgrep node)\`, \`pkill node\`, or \`killall node\`; use a specific PID or process group id where supported.
|
||||
- Use foreground execution (is_background: false) for:
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
|
|
|
|||
|
|
@ -4740,6 +4740,8 @@ function getShellToolDescription(): string {
|
|||
const processGroupNote = isWindows
|
||||
? ''
|
||||
: '\n - Command is executed as a subprocess that leads its own process group. Command process group can be terminated as `kill -- -PGID` or signaled as `kill -s SIGNAL -- -PGID`.';
|
||||
const processStopNote =
|
||||
'\n - To stop a background command started by this tool, use `task_stop` when a task id is available. Do not use broad process-name kills such as `kill $(pgrep node)`, `pkill node`, or `killall node`; use a specific PID or process group id where supported.';
|
||||
|
||||
return `Executes a given shell command (as \`${executionWrapper}\`) in a subprocess with optional timeout, ensuring proper handling and security measures.
|
||||
|
||||
|
|
@ -4775,7 +4777,7 @@ ${getShellCommandSequencingGuidance(shellConfiguration)}
|
|||
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
|
||||
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
|
||||
- Any command expected to run indefinitely until manually stopped
|
||||
${processGroupNote}
|
||||
${processGroupNote}${processStopNote}
|
||||
- Use foreground execution (is_background: false) for:
|
||||
- One-time commands: \`ls\`, \`cat\`, \`grep\`
|
||||
- Build commands: \`npm run build\`, \`make\`
|
||||
|
|
|
|||
|
|
@ -606,6 +606,18 @@ describe('detectSelfKillCommand', () => {
|
|||
expect(detectSelfKillCommand('command -p killall node')).toBe(true);
|
||||
});
|
||||
|
||||
it('detects kill commands using pgrep selectors for qwen-code hosts', () => {
|
||||
expect(detectSelfKillCommand('kill -9 $(pgrep node)')).toBe(true);
|
||||
expect(detectSelfKillCommand('kill $(pgrep -f node)')).toBe(true);
|
||||
expect(detectSelfKillCommand('kill -9 $(pgrep node | head -1)')).toBe(true);
|
||||
expect(detectSelfKillCommand('kill -9 `pgrep node | head -1`')).toBe(true);
|
||||
expect(detectSelfKillCommand('pgrep node | xargs kill')).toBe(true);
|
||||
expect(detectSelfKillCommand('pgrep node | xargs sudo kill')).toBe(true);
|
||||
expect(detectSelfKillCommand('pgrep node | xargs -I {} kill -9 {}')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('detects taskkill inline and dash-prefixed image options', () => {
|
||||
expect(detectSelfKillCommand('taskkill /IM:node.exe /F')).toBe(true);
|
||||
expect(
|
||||
|
|
@ -648,6 +660,10 @@ describe('detectSelfKillCommand', () => {
|
|||
expect(detectSelfKillCommand('pkill -f vite')).toBe(false);
|
||||
expect(detectSelfKillCommand('pkill -f "node server.js"')).toBe(false);
|
||||
expect(detectSelfKillCommand('pkill -9f "node server.js"')).toBe(false);
|
||||
expect(detectSelfKillCommand('kill -9 $(pgrep vite)')).toBe(false);
|
||||
expect(detectSelfKillCommand('kill -9 $(pgrep -f "node server.js")')).toBe(
|
||||
false,
|
||||
);
|
||||
expect(detectSelfKillCommand('pkill -F qwen-code.pid vite')).toBe(false);
|
||||
expect(detectSelfKillCommand('taskkill /IM notepad.exe')).toBe(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -208,6 +208,8 @@ export function splitCommands(command: string): string[] {
|
|||
let currentCommand = '';
|
||||
let inSingleQuotes = false;
|
||||
let inDoubleQuotes = false;
|
||||
let inBackticks = false;
|
||||
let substitutionDepth = 0;
|
||||
let i = 0;
|
||||
|
||||
const previousNonWhitespaceChar = (index: number): string | undefined => {
|
||||
|
|
@ -235,13 +237,42 @@ export function splitCommands(command: string): string[] {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (char === "'" && !inDoubleQuotes) {
|
||||
if (!inSingleQuotes && char === '`') {
|
||||
inBackticks = !inBackticks;
|
||||
} else if (
|
||||
!inSingleQuotes &&
|
||||
!inBackticks &&
|
||||
char === '$' &&
|
||||
nextChar === '('
|
||||
) {
|
||||
substitutionDepth++;
|
||||
currentCommand += '$(';
|
||||
i += 2;
|
||||
continue;
|
||||
} else if (!inBackticks && substitutionDepth > 0 && char === ')') {
|
||||
substitutionDepth--;
|
||||
} else if (
|
||||
!inBackticks &&
|
||||
substitutionDepth === 0 &&
|
||||
char === "'" &&
|
||||
!inDoubleQuotes
|
||||
) {
|
||||
inSingleQuotes = !inSingleQuotes;
|
||||
} else if (char === '"' && !inSingleQuotes) {
|
||||
} else if (
|
||||
!inBackticks &&
|
||||
substitutionDepth === 0 &&
|
||||
char === '"' &&
|
||||
!inSingleQuotes
|
||||
) {
|
||||
inDoubleQuotes = !inDoubleQuotes;
|
||||
}
|
||||
|
||||
if (!inSingleQuotes && !inDoubleQuotes) {
|
||||
if (
|
||||
!inSingleQuotes &&
|
||||
!inDoubleQuotes &&
|
||||
!inBackticks &&
|
||||
substitutionDepth === 0
|
||||
) {
|
||||
if (
|
||||
(char === '&' && nextChar === '&') ||
|
||||
(char === '|' && (nextChar === '|' || nextChar === '&'))
|
||||
|
|
@ -460,6 +491,28 @@ const PKILL_OPTIONS_WITH_VALUES = new Set([
|
|||
'--euid',
|
||||
]);
|
||||
|
||||
const XARGS_OPTIONS_WITH_VALUES = new Set([
|
||||
'-I',
|
||||
'-n',
|
||||
'-P',
|
||||
'-s',
|
||||
'-E',
|
||||
'-d',
|
||||
'-L',
|
||||
'-l',
|
||||
'-a',
|
||||
'-J',
|
||||
'-R',
|
||||
'--replace',
|
||||
'--max-args',
|
||||
'--max-procs',
|
||||
'--max-chars',
|
||||
'--eof',
|
||||
'--delimiter',
|
||||
'--max-lines',
|
||||
'--arg-file',
|
||||
]);
|
||||
|
||||
export const SHELL_SELF_KILL_REJECTION =
|
||||
'Blocked: this command may terminate the running qwen-code process because it targets all node/qwen-code processes. Use task_stop for managed background shells, or kill a specific PID instead.';
|
||||
|
||||
|
|
@ -736,13 +789,126 @@ function pkillTargetsSelf(tokens: string[]): boolean {
|
|||
return args.some((arg) => matchesSelfProcessPattern(arg));
|
||||
}
|
||||
|
||||
function pgrepTargetsSelf(tokens: string[]): boolean {
|
||||
return pkillTargetsSelf(['pkill', ...tokens.slice(1)]);
|
||||
}
|
||||
|
||||
function pgrepSubstitutionArgs(segment: string): string[] {
|
||||
let quote: '"' | "'" | '' = '';
|
||||
let escaped = false;
|
||||
const args: string[] = [];
|
||||
|
||||
for (let index = 0; index < segment.length; index++) {
|
||||
const char = segment[index]!;
|
||||
|
||||
if (quote === "'") {
|
||||
if (char === "'") quote = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
quote = quote === '"' ? '' : '"';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "'" && quote === '') {
|
||||
quote = "'";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '`') {
|
||||
let end = index + 1;
|
||||
let innerEscaped = false;
|
||||
for (; end < segment.length; end++) {
|
||||
const innerChar = segment[end]!;
|
||||
if (innerEscaped) {
|
||||
innerEscaped = false;
|
||||
continue;
|
||||
}
|
||||
if (innerChar === '\\') {
|
||||
innerEscaped = true;
|
||||
continue;
|
||||
}
|
||||
if (innerChar === '`') break;
|
||||
}
|
||||
if (end >= segment.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
const inner = segment.slice(index + 1, end).trim();
|
||||
const match = inner.match(/^pgrep\b(.*)$/i);
|
||||
if (match) {
|
||||
args.push(match[1] ?? '');
|
||||
}
|
||||
index = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char !== '$' || segment[index + 1] !== '(') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const end = segment.indexOf(')', index + 2);
|
||||
if (end === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const inner = segment.slice(index + 2, end).trim();
|
||||
const match = inner.match(/^pgrep\b(.*)$/i);
|
||||
if (match) {
|
||||
args.push(match[1] ?? '');
|
||||
}
|
||||
index = end;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function killCommandTargetsSelf(segment: string): boolean {
|
||||
return pgrepSubstitutionArgs(segment).some((args) => {
|
||||
const parsed = parseShellSegment(`pgrep ${args}`);
|
||||
return parsed !== null && pgrepTargetsSelf(parsed);
|
||||
});
|
||||
}
|
||||
|
||||
function xargsInvokesKill(segment: string | undefined): boolean {
|
||||
if (!segment) {
|
||||
return false;
|
||||
}
|
||||
const parsed = parseShellSegment(segment);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
const tokens = unwrapExecutionPrefixes(parsed);
|
||||
if (normalizeExecutableName(tokens[0] ?? '') !== 'xargs') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commandTokens = unwrapExecutionPrefixes(
|
||||
commandArguments(tokens, XARGS_OPTIONS_WITH_VALUES),
|
||||
);
|
||||
const command = commandTokens.find((token) => !isOptionToken(token));
|
||||
return normalizeExecutableName(command ?? '') === 'kill';
|
||||
}
|
||||
|
||||
export function detectSelfKillCommand(command: string): boolean {
|
||||
if (!/kill/i.test(command)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const segments = getCommandSegments(command);
|
||||
for (const segment of segments) {
|
||||
for (let index = 0; index < segments.length; index++) {
|
||||
const segment = segments[index]!;
|
||||
const parsed = parseShellSegment(segment);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
|
|
@ -763,6 +929,16 @@ export function detectSelfKillCommand(command: string): boolean {
|
|||
if (root === 'pkill' && pkillTargetsSelf(tokens)) {
|
||||
return true;
|
||||
}
|
||||
if (root === 'kill' && killCommandTargetsSelf(segment)) {
|
||||
return true;
|
||||
}
|
||||
if (root === 'pgrep' && pgrepTargetsSelf(tokens)) {
|
||||
for (let j = index + 1; j < segments.length; j++) {
|
||||
if (xargsInvokesKill(segments[j])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue