mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 05:02:14 +00:00
Fix bash abort to kill entire process tree immediately
- Switch from exec() to spawn() with detached: true - Create new process group for spawned commands - Kill entire process group with process.kill(-pid) on abort - This ensures commands like "sleep 4 && echo hello" abort immediately - Previous implementation only killed parent shell, leaving subprocesses running
This commit is contained in:
parent
ea5097e13e
commit
6e9fa8dde1
1 changed files with 83 additions and 31 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import type { AgentTool } from "@mariozechner/pi-ai";
|
import type { AgentTool } from "@mariozechner/pi-ai";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import { exec } from "child_process";
|
import { spawn } from "child_process";
|
||||||
|
|
||||||
const bashSchema = Type.Object({
|
const bashSchema = Type.Object({
|
||||||
command: Type.String({ description: "Bash command to execute" }),
|
command: Type.String({ description: "Bash command to execute" }),
|
||||||
|
|
@ -14,13 +14,46 @@ export const bashTool: AgentTool<typeof bashSchema> = {
|
||||||
parameters: bashSchema,
|
parameters: bashSchema,
|
||||||
execute: async (_toolCallId: string, { command }: { command: string }, signal?: AbortSignal) => {
|
execute: async (_toolCallId: string, { command }: { command: string }, signal?: AbortSignal) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = exec(
|
const child = spawn("sh", ["-c", command], {
|
||||||
command,
|
detached: true,
|
||||||
{
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
timeout: 30000,
|
});
|
||||||
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
||||||
},
|
let stdout = "";
|
||||||
(error, stdout, stderr) => {
|
let stderr = "";
|
||||||
|
let timedOut = false;
|
||||||
|
|
||||||
|
// Set timeout
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
onAbort();
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
// Collect stdout
|
||||||
|
if (child.stdout) {
|
||||||
|
child.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
// Limit buffer size
|
||||||
|
if (stdout.length > 10 * 1024 * 1024) {
|
||||||
|
stdout = stdout.slice(0, 10 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect stderr
|
||||||
|
if (child.stderr) {
|
||||||
|
child.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
// Limit buffer size
|
||||||
|
if (stderr.length > 10 * 1024 * 1024) {
|
||||||
|
stderr = stderr.slice(0, 10 * 1024 * 1024);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle process exit
|
||||||
|
child.on("close", (code) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
if (signal) {
|
if (signal) {
|
||||||
signal.removeEventListener("abort", onAbort);
|
signal.removeEventListener("abort", onAbort);
|
||||||
}
|
}
|
||||||
|
|
@ -33,24 +66,43 @@ export const bashTool: AgentTool<typeof bashSchema> = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (timedOut) {
|
||||||
|
resolve({
|
||||||
|
output: `Command timed out after 30 seconds\nSTDOUT: ${stdout}\nSTDERR: ${stderr}`,
|
||||||
|
details: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let output = "";
|
let output = "";
|
||||||
if (stdout) output += stdout;
|
if (stdout) output += stdout;
|
||||||
if (stderr) output += stderr ? `\nSTDERR:\n${stderr}` : "";
|
if (stderr) output += stderr ? `\nSTDERR:\n${stderr}` : "";
|
||||||
|
|
||||||
if (error && !error.killed) {
|
if (code !== 0 && code !== null) {
|
||||||
resolve({
|
resolve({
|
||||||
output: `Error executing command: ${error.message}\n${output}`,
|
output: `Command exited with code ${code}\n${output}`,
|
||||||
details: undefined,
|
details: undefined,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
resolve({ output: output || "(no output)", details: undefined });
|
resolve({ output: output || "(no output)", details: undefined });
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Handle abort signal
|
// Handle abort signal - kill entire process tree
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
|
if (child.pid) {
|
||||||
|
// Kill the entire process group (negative PID kills all processes in the group)
|
||||||
|
try {
|
||||||
|
process.kill(-child.pid, "SIGKILL");
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to killing just the child if process group kill fails
|
||||||
|
try {
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
|
} catch (e2) {
|
||||||
|
// Process already dead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (signal) {
|
if (signal) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue