Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
a4afa11642 [autofix.ci] apply automated fixes 2025-08-25 08:49:16 +00:00
Claude Bot
ecb11528d4 Add .kill() method to ShellPromise for terminating processes
This adds a new .kill(signal?) method to ShellPromise that allows users to terminate long-running shell processes.

Features:
- Kill processes with default SIGTERM (15)
- Accept numeric signal codes: .kill(9) for SIGKILL
- Accept named signals: .kill("SIGTERM") or .kill("TERM")
- Returns boolean indicating if any processes were killed
- Safe to call on processes that haven't started (returns false)
- Safe to call on processes that already exited (returns false)
- Properly handles pipelines by tracking all active subprocesses

Implementation:
- Added subprocess tracking to Interpreter via active_subprocesses list
- Added registerSubprocess/unregisterSubprocess methods
- Modified Cmd state to register/unregister subprocesses on spawn/cleanup
- Added JavaScript kill method with signal parsing and validation
- Added C++ binding for kill method in ShellInterpreter
- Added comprehensive test suite covering all use cases

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 08:47:30 +00:00
5 changed files with 253 additions and 0 deletions

View File

@@ -23,6 +23,10 @@ export default [
fn: "getStarted",
length: 0,
},
kill: {
fn: "kill",
length: 1,
},
},
}),
];

View File

@@ -109,6 +109,7 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
#throws: boolean = true;
#resolve: (code: number, stdout: Buffer, stderr: Buffer) => void;
#reject: (code: number, stdout: Buffer, stderr: Buffer) => void;
#interpreter: $ZigGeneratedClasses.ShellInterpreter | undefined = undefined;
constructor(args: $ZigGeneratedClasses.ParsedShellScript, throws: boolean) {
// Create the error immediately so it captures the stacktrace at the point
@@ -170,6 +171,7 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
this.#hasRun = true;
let interp = createShellInterpreter(this.#resolve, this.#reject, this.#args!);
this.#interpreter = interp;
this.#args = undefined;
interp.run();
}
@@ -238,6 +240,41 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
return this;
}
kill(signal?: number | string): boolean {
if (!this.#hasRun || !this.#interpreter) {
return false;
}
let sig = 15; // SIGTERM by default
if (typeof signal === "number") {
sig = signal;
} else if (typeof signal === "string") {
// Convert named signals to numbers
const namedSignals: Record<string, number> = {
"SIGTERM": 15,
"SIGKILL": 9,
"SIGINT": 2,
"SIGQUIT": 3,
"SIGHUP": 1,
"SIGUSR1": 10,
"SIGUSR2": 12,
};
const upperSignal = signal.toUpperCase();
const normalizedSignal = upperSignal.startsWith("SIG") ? upperSignal : `SIG${upperSignal}`;
if (namedSignals[normalizedSignal] !== undefined) {
sig = namedSignals[normalizedSignal];
} else {
throw new Error(`Unknown signal: ${signal}`);
}
} else if (signal !== undefined) {
throw new TypeError("Signal must be a number or string");
}
return this.#interpreter.kill(sig);
}
then(onfulfilled, onrejected) {
this.#run();

View File

@@ -266,6 +266,9 @@ pub const Interpreter = struct {
vm_args_utf8: std.ArrayList(jsc.ZigString.Slice),
async_commands_executing: u32 = 0,
/// List of active subprocesses for kill() support
active_subprocesses: std.ArrayList(*Subprocess) = std.ArrayList(*Subprocess).init(undefined),
globalThis: *jsc.JSGlobalObject,
flags: packed struct(u8) {
@@ -863,6 +866,7 @@ pub const Interpreter = struct {
},
.vm_args_utf8 = std.ArrayList(jsc.ZigString.Slice).init(bun.default_allocator),
.active_subprocesses = std.ArrayList(*Subprocess).init(bun.default_allocator),
.__alloc_scope = if (bun.Environment.enableAllocScopes) bun.AllocationScope.init(allocator) else {},
.globalThis = undefined,
};
@@ -1201,6 +1205,7 @@ pub const Interpreter = struct {
str.deinit();
}
this.vm_args_utf8.deinit();
this.active_subprocesses.deinit();
this.this_jsvalue = .zero;
this.allocator.destroy(this);
}
@@ -1273,6 +1278,31 @@ pub const Interpreter = struct {
return jsc.JSValue.jsBoolean(this.started.load(.seq_cst));
}
pub fn kill(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
_ = globalThis; // autofix
const args_ = callframe.arguments_old(1);
const args = args_.ptr[0..args_.len];
const signal: i32 = if (args.len > 0 and args[0].isNumber())
args[0].toInt32()
else
15; // SIGTERM by default
var killed_count: u32 = 0;
// Kill all active subprocesses
for (this.active_subprocesses.items) |subprocess| {
if (!subprocess.hasExited()) {
switch (subprocess.tryKill(signal)) {
.result => killed_count += 1,
.err => {}, // Ignore kill errors (process might have already exited)
}
}
}
return jsc.JSValue.jsBoolean(killed_count > 0);
}
pub fn getBufferedStdout(this: *ThisInterpreter, globalThis: *JSGlobalObject) jsc.JSValue {
return ioToJSValue(globalThis, this.root_shell.buffered_stdout());
}
@@ -1290,6 +1320,21 @@ pub const Interpreter = struct {
return this.has_pending_activity.load(.seq_cst) > 0;
}
pub fn registerSubprocess(this: *ThisInterpreter, subprocess: *Subprocess) void {
this.active_subprocesses.append(subprocess) catch {
// If we can't track it, just continue - the kill method won't find it but it's not critical
};
}
pub fn unregisterSubprocess(this: *ThisInterpreter, subprocess: *Subprocess) void {
for (this.active_subprocesses.items, 0..) |proc, i| {
if (proc == subprocess) {
_ = this.active_subprocesses.swapRemove(i);
break;
}
}
}
fn incrPendingActivityFlag(has_pending_activity: *std.atomic.Value(u32)) void {
_ = has_pending_activity.fetchAdd(1, .seq_cst);
log("Interpreter incr pending activity {d}", .{has_pending_activity.load(.seq_cst)});
@@ -1975,6 +2020,7 @@ const JSValue = bun.jsc.JSValue;
const shell = bun.shell;
const Yield = shell.Yield;
const ast = shell.AST;
const Subprocess = shell.subproc.ShellSubprocess;
const windows = bun.windows;
const uv = windows.libuv;

View File

@@ -528,6 +528,9 @@ fn initSubproc(this: *Cmd) Yield {
},
};
subproc.ref();
// Register subprocess for kill() support
this.base.interpreter.registerSubprocess(subproc);
this.spawn_arena_freed = true;
arena.deinit();
@@ -698,6 +701,10 @@ pub fn deinit(this: *Cmd) void {
if (this.exec != .none) {
if (this.exec == .subproc) {
var cmd = this.exec.subproc.child;
// Unregister subprocess from interpreter tracking
this.base.interpreter.unregisterSubprocess(cmd);
if (cmd.hasExited()) {
cmd.unref(true);
} else {

View File

@@ -0,0 +1,159 @@
import { $ } from "bun";
import { describe, expect, test } from "bun:test";
describe("Shell kill() method", () => {
test("should be able to kill a long-running process", async () => {
const proc = $`sleep 10`;
// Start the process
const promise = proc.run();
// Give it a moment to start
await Bun.sleep(100);
// Kill the process
const killed = proc.kill();
expect(killed).toBe(true);
// The process should exit with an error due to being killed
const result = await promise;
expect(result.exitCode).not.toBe(0);
});
test("should be able to kill with specific signal", async () => {
const proc = $`sleep 10`;
// Start the process
const promise = proc.run();
// Give it a moment to start
await Bun.sleep(100);
// Kill the process with SIGKILL
const killed = proc.kill(9);
expect(killed).toBe(true);
// The process should exit with an error due to being killed
const result = await promise;
expect(result.exitCode).not.toBe(0);
});
test("should be able to kill with named signal", async () => {
const proc = $`sleep 10`;
// Start the process
const promise = proc.run();
// Give it a moment to start
await Bun.sleep(100);
// Kill the process with SIGTERM
const killed = proc.kill("SIGTERM");
expect(killed).toBe(true);
// The process should exit with an error due to being killed
const result = await promise;
expect(result.exitCode).not.toBe(0);
});
test("should be able to kill with signal name without SIG prefix", async () => {
const proc = $`sleep 10`;
// Start the process
const promise = proc.run();
// Give it a moment to start
await Bun.sleep(100);
// Kill the process with TERM (without SIG prefix)
const killed = proc.kill("TERM");
expect(killed).toBe(true);
// The process should exit with an error due to being killed
const result = await promise;
expect(result.exitCode).not.toBe(0);
});
test("should return false when trying to kill a process that hasn't started", () => {
const proc = $`echo hello`;
// Try to kill before starting
const killed = proc.kill();
expect(killed).toBe(false);
});
test("should return false when trying to kill a process that already exited", async () => {
const proc = $`echo hello`;
// Start and wait for process to finish
await proc.run();
// Try to kill after it's done (should return false)
const killed = proc.kill();
expect(killed).toBe(false);
});
test("should handle invalid signal names", async () => {
const proc = $`sleep 10`;
// Start the process
const promise = proc.run();
// Give it a moment to start
await Bun.sleep(100);
// Try to kill with invalid signal
expect(() => proc.kill("INVALID")).toThrow("Unknown signal: INVALID");
// Clean up
proc.kill();
await promise.catch(() => {});
});
test("should handle invalid signal type", async () => {
const proc = $`sleep 10`;
// Start the process
const promise = proc.run();
// Give it a moment to start
await Bun.sleep(100);
// Try to kill with invalid signal type
expect(() => proc.kill({} as any)).toThrow("Signal must be a number or string");
// Clean up
proc.kill();
await promise.catch(() => {});
});
test("should be able to kill multiple processes in a pipeline", async () => {
const proc = $`sleep 10 | sleep 10`;
// Start the pipeline
const promise = proc.run();
// Give it a moment to start
await Bun.sleep(100);
// Kill the pipeline
const killed = proc.kill();
expect(killed).toBe(true);
// The pipeline should exit with an error due to being killed
const result = await promise;
expect(result.exitCode).not.toBe(0);
});
test("should work with promises (await)", async () => {
const proc = $`sleep 10`;
// Start process via await
setTimeout(() => {
proc.kill();
}, 100);
const result = await proc;
expect(result.exitCode).not.toBe(0);
});
});