mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 23:48:52 +00:00
Compare commits
8 Commits
claude/imp
...
claude/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6944b0d6a5 | ||
|
|
095e9318e2 | ||
|
|
09b171f509 | ||
|
|
3aad0bdbe5 | ||
|
|
77b2796d9b | ||
|
|
5433cdb1c4 | ||
|
|
3a842c3c8e | ||
|
|
2a00699f8e |
@@ -23,6 +23,10 @@ export default [
|
||||
fn: "getStarted",
|
||||
length: 0,
|
||||
},
|
||||
kill: {
|
||||
fn: "killFromJS",
|
||||
length: 1,
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -109,6 +109,8 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
|
||||
#throws: boolean = true;
|
||||
#resolve: (code: number, stdout: Buffer, stderr: Buffer) => void;
|
||||
#reject: (code: number, stdout: Buffer, stderr: Buffer) => void;
|
||||
#interp: $ZigGeneratedClasses.ShellInterpreter | undefined = undefined;
|
||||
#killSignal: number | undefined = undefined;
|
||||
|
||||
constructor(args: $ZigGeneratedClasses.ParsedShellScript, throws: boolean) {
|
||||
// Create the error immediately so it captures the stacktrace at the point
|
||||
@@ -170,11 +172,30 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
|
||||
this.#hasRun = true;
|
||||
|
||||
let interp = createShellInterpreter(this.#resolve, this.#reject, this.#args!);
|
||||
this.#interp = interp;
|
||||
this.#args = undefined;
|
||||
|
||||
// If killed before starting, kill immediately
|
||||
if (this.#killSignal !== undefined) {
|
||||
interp.kill(this.#killSignal);
|
||||
}
|
||||
|
||||
interp.run();
|
||||
}
|
||||
}
|
||||
|
||||
kill(signal?: number): void {
|
||||
const killSignal = signal ?? 9;
|
||||
if (this.#interp) {
|
||||
this.#interp.kill(killSignal);
|
||||
} else {
|
||||
// Only save kill signal if not already killed
|
||||
if (this.#killSignal === undefined) {
|
||||
this.#killSignal = killSignal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#quiet(): this {
|
||||
this.#throwIfRunning();
|
||||
this.#args!.setQuiet();
|
||||
|
||||
@@ -605,6 +605,9 @@ pub inline fn parentCmdMut(this: *Builtin) *Cmd {
|
||||
}
|
||||
|
||||
pub fn done(this: *Builtin, exit_code: anytype) Yield {
|
||||
// Guard against concurrent calls (e.g., normal completion racing with kill())
|
||||
if (this.exit_code != null) return .done;
|
||||
|
||||
const code: ExitCode = switch (@TypeOf(exit_code)) {
|
||||
bun.sys.E => @intFromEnum(exit_code),
|
||||
u1, u8, u16 => exit_code,
|
||||
@@ -707,7 +710,8 @@ pub fn writeNoIO(this: *Builtin, comptime io_kind: @Type(.enum_literal), buf: []
|
||||
len;
|
||||
|
||||
const slice = io.arraybuf.buf.slice()[io.arraybuf.i .. io.arraybuf.i + write_len];
|
||||
@memcpy(slice, buf[0..write_len]);
|
||||
// Use copyForwards to handle potential overlap (e.g., when buf points to same arraybuf)
|
||||
std.mem.copyForwards(u8, slice, buf[0..write_len]);
|
||||
io.arraybuf.i +|= @truncate(write_len);
|
||||
log("{s} write to arraybuf {d}\n", .{ @tagName(this.kind), write_len });
|
||||
return Maybe(usize).initResult(write_len);
|
||||
|
||||
@@ -271,10 +271,12 @@ pub const Interpreter = struct {
|
||||
flags: packed struct(u8) {
|
||||
done: bool = false,
|
||||
quiet: bool = false,
|
||||
__unused: u6 = 0,
|
||||
killed: bool = false,
|
||||
__unused: u5 = 0,
|
||||
} = .{},
|
||||
exit_code: ?ExitCode = 0,
|
||||
this_jsvalue: JSValue = .zero,
|
||||
root_script: ?*Script = null,
|
||||
|
||||
__alloc_scope: if (bun.Environment.enableAllocScopes) bun.AllocationScope else void,
|
||||
|
||||
@@ -1074,12 +1076,23 @@ pub const Interpreter = struct {
|
||||
}
|
||||
|
||||
pub fn run(this: *ThisInterpreter) !Maybe(void) {
|
||||
log("Interpreter(0x{x}) run", .{@intFromPtr(this)});
|
||||
log("Interpreter(0x{x}) run killed={}", .{ @intFromPtr(this), this.flags.killed });
|
||||
|
||||
// Check if killed before starting
|
||||
if (this.flags.killed) {
|
||||
log("Interpreter(0x{x}) was killed before starting, finishing immediately", .{@intFromPtr(this)});
|
||||
incrPendingActivityFlag(&this.has_pending_activity);
|
||||
const exit_code = this.exit_code orelse 137; // Default to SIGKILL
|
||||
_ = this.finish(exit_code);
|
||||
return .success;
|
||||
}
|
||||
|
||||
if (this.setupIOBeforeRun().asErr()) |e| {
|
||||
return .{ .err = e };
|
||||
}
|
||||
|
||||
var root = Script.init(this, &this.root_shell, &this.args.script_ast, Script.ParentPtr.init(this), this.root_io.copy());
|
||||
this.root_script = root;
|
||||
this.started.store(true, .seq_cst);
|
||||
root.start().run();
|
||||
|
||||
@@ -1087,9 +1100,16 @@ pub const Interpreter = struct {
|
||||
}
|
||||
|
||||
pub fn runFromJS(this: *ThisInterpreter, globalThis: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue {
|
||||
log("Interpreter(0x{x}) runFromJS", .{@intFromPtr(this)});
|
||||
_ = callframe; // autofix
|
||||
|
||||
// Check if killed before starting
|
||||
if (this.flags.killed) {
|
||||
incrPendingActivityFlag(&this.has_pending_activity);
|
||||
const exit_code = this.exit_code orelse 137; // Default to SIGKILL
|
||||
_ = this.finish(exit_code);
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
if (this.setupIOBeforeRun().asErr()) |e| {
|
||||
defer this.deinitEverything();
|
||||
const shellerr = bun.shell.ShellErr.newSys(e);
|
||||
@@ -1098,6 +1118,7 @@ pub const Interpreter = struct {
|
||||
incrPendingActivityFlag(&this.has_pending_activity);
|
||||
|
||||
var root = Script.init(this, &this.root_shell, &this.args.script_ast, Script.ParentPtr.init(this), this.root_io.copy());
|
||||
this.root_script = root;
|
||||
this.started.store(true, .seq_cst);
|
||||
root.start().run();
|
||||
if (globalThis.hasException()) return error.JSError;
|
||||
@@ -1128,8 +1149,15 @@ pub const Interpreter = struct {
|
||||
if (child.ptr.is(Script)) {
|
||||
const script = child.as(Script);
|
||||
script.deinitFromInterpreter();
|
||||
this.exit_code = exit_code;
|
||||
if (this.async_commands_executing == 0) return this.finish(exit_code);
|
||||
|
||||
// Clear the root_script pointer to prevent use-after-free
|
||||
this.root_script = null;
|
||||
|
||||
// Use the kill exit code if we were killed, otherwise use the script's exit code
|
||||
const final_exit_code = if (this.flags.killed) (this.exit_code orelse exit_code) else exit_code;
|
||||
this.exit_code = final_exit_code;
|
||||
|
||||
if (this.async_commands_executing == 0) return this.finish(final_exit_code);
|
||||
return .suspended;
|
||||
}
|
||||
@panic("Bad child");
|
||||
@@ -1273,6 +1301,36 @@ pub const Interpreter = struct {
|
||||
return jsc.JSValue.jsBoolean(this.started.load(.seq_cst));
|
||||
}
|
||||
|
||||
pub fn killFromJS(this: *ThisInterpreter, _: *JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
|
||||
const sig_arg = callframe.argument(0);
|
||||
const signal_raw: i32 = if (sig_arg.isNumber()) sig_arg.to(i32) else @intFromEnum(bun.SignalCode.SIGKILL);
|
||||
|
||||
// Validate signal is in valid range (1-31 for standard signals)
|
||||
const signal: i32 = if (signal_raw < 1 or signal_raw > 31) @intFromEnum(bun.SignalCode.SIGKILL) else signal_raw;
|
||||
|
||||
// If already done or killed, do nothing
|
||||
if (this.flags.done or this.flags.killed) {
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
// Mark as killed and set exit code
|
||||
this.flags.killed = true;
|
||||
const exit_code: ExitCode = 128 + @as(u8, @intCast(signal));
|
||||
this.exit_code = exit_code;
|
||||
|
||||
// If not started yet, it will check flags.killed when it starts and finish immediately
|
||||
if (!this.started.load(.seq_cst)) {
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
// Kill all active processes/builtins in the execution tree
|
||||
if (this.root_script) |root| {
|
||||
root.kill(signal);
|
||||
}
|
||||
|
||||
return .js_undefined;
|
||||
}
|
||||
|
||||
pub fn getBufferedStdout(this: *ThisInterpreter, globalThis: *JSGlobalObject) jsc.JSValue {
|
||||
return ioToJSValue(globalThis, this.root_shell.buffered_stdout());
|
||||
}
|
||||
|
||||
@@ -142,6 +142,23 @@ pub fn deinit(this: *Async) void {
|
||||
_ = this;
|
||||
}
|
||||
|
||||
pub fn kill(this: *Async, signal: i32) void {
|
||||
log("{} kill sig={d}", .{ this, signal });
|
||||
if (this.state == .exec) {
|
||||
if (this.state.exec.child) |child| {
|
||||
if (child.ptr.is(Cmd)) {
|
||||
child.as(Cmd).kill(signal);
|
||||
} else if (child.ptr.is(Pipeline)) {
|
||||
child.as(Pipeline).kill(signal);
|
||||
} else if (child.ptr.is(If)) {
|
||||
child.as(If).kill(signal);
|
||||
} else if (child.ptr.is(CondExpr)) {
|
||||
child.as(CondExpr).kill(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn actuallyDeinit(this: *Async) void {
|
||||
this.io.deref();
|
||||
bun.destroy(this);
|
||||
|
||||
@@ -141,6 +141,27 @@ pub fn childDone(this: *Binary, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return this.parent.childDone(this, exit_code);
|
||||
}
|
||||
|
||||
pub fn kill(this: *Binary, signal: i32) void {
|
||||
log("Binary(0x{x}) kill sig={d}", .{ @intFromPtr(this), signal });
|
||||
if (this.currently_executing) |child| {
|
||||
if (child.ptr.is(Cmd)) {
|
||||
child.as(Cmd).kill(signal);
|
||||
} else if (child.ptr.is(Pipeline)) {
|
||||
child.as(Pipeline).kill(signal);
|
||||
} else if (child.ptr.is(Binary)) {
|
||||
child.as(Binary).kill(signal);
|
||||
} else if (child.ptr.is(Async)) {
|
||||
child.as(Async).kill(signal);
|
||||
} else if (child.ptr.is(Subshell)) {
|
||||
child.as(Subshell).kill(signal);
|
||||
} else if (child.ptr.is(If)) {
|
||||
child.as(If).kill(signal);
|
||||
} else if (child.ptr.is(CondExpr)) {
|
||||
child.as(CondExpr).kill(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Binary) void {
|
||||
if (this.currently_executing) |child| {
|
||||
child.deinit();
|
||||
|
||||
@@ -213,6 +213,23 @@ pub fn isSubproc(this: *Cmd) bool {
|
||||
return this.exec == .subproc;
|
||||
}
|
||||
|
||||
pub fn kill(this: *Cmd, signal: i32) void {
|
||||
log("Cmd(0x{x}, {s}) kill sig={d}", .{ @intFromPtr(this), @tagName(this.exec), signal });
|
||||
switch (this.exec) {
|
||||
.subproc => |*subproc| {
|
||||
if (!subproc.child.hasExited()) {
|
||||
_ = subproc.child.tryKill(signal);
|
||||
}
|
||||
},
|
||||
.bltn => |*bltn| {
|
||||
if (bltn.exit_code == null) {
|
||||
_ = bltn.done(@as(ExitCode, @intCast(128 + @as(u8, @intCast(signal)))));
|
||||
}
|
||||
},
|
||||
.none => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// If starting a command results in an error (failed to find executable in path for example)
|
||||
/// then it should write to the stderr of the entire shell script process
|
||||
pub fn writeFailingError(this: *Cmd, comptime fmt: []const u8, args: anytype) Yield {
|
||||
|
||||
@@ -210,6 +210,13 @@ fn doStat(this: *CondExpr) Yield {
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
pub fn kill(this: *CondExpr, signal: i32) void {
|
||||
log("CondExpr(0x{x}) kill sig={d}", .{ @intFromPtr(this), signal });
|
||||
if (this.state == .expanding_args) {
|
||||
this.state.expanding_args.expansion.kill(signal);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *CondExpr) void {
|
||||
this.io.deinit();
|
||||
for (this.args.items) |item| {
|
||||
|
||||
@@ -146,6 +146,13 @@ pub fn init(
|
||||
expansion.current_out = std.ArrayList(u8).init(expansion.base.allocator());
|
||||
}
|
||||
|
||||
pub fn kill(this: *Expansion, signal: i32) void {
|
||||
log("Expansion(0x{x}) kill sig={d}", .{ @intFromPtr(this), signal });
|
||||
if (this.child_state == .cmd_subst) {
|
||||
this.child_state.cmd_subst.cmd.kill(signal);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(expansion: *Expansion) void {
|
||||
log("Expansion(0x{x}) deinit", .{@intFromPtr(expansion)});
|
||||
expansion.current_out.deinit();
|
||||
|
||||
@@ -4,6 +4,7 @@ base: State,
|
||||
node: *const ast.If,
|
||||
parent: ParentPtr,
|
||||
io: IO,
|
||||
currently_executing: ?*Stmt = null,
|
||||
state: union(enum) {
|
||||
idle,
|
||||
exec: struct {
|
||||
@@ -138,6 +139,7 @@ pub fn next(this: *If) Yield {
|
||||
this.state.exec.stmt_idx += 1;
|
||||
const stmt = this.state.exec.stmts.getConst(idx);
|
||||
var newstmt = Stmt.init(this.base.interpreter, this.base.shell, stmt, this, this.io.copy());
|
||||
this.currently_executing = newstmt;
|
||||
return newstmt.start();
|
||||
},
|
||||
.waiting_write_err => return .suspended, // yield execution
|
||||
@@ -148,6 +150,13 @@ pub fn next(this: *If) Yield {
|
||||
return this.parent.childDone(this, 0);
|
||||
}
|
||||
|
||||
pub fn kill(this: *If, signal: i32) void {
|
||||
log("{} kill sig={d}", .{ this, signal });
|
||||
if (this.currently_executing) |stmt| {
|
||||
stmt.kill(signal);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *If) void {
|
||||
log("{} deinit", .{this});
|
||||
this.io.deref();
|
||||
@@ -162,6 +171,7 @@ pub fn childDone(this: *If, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
@panic("Expected `exec` state in If, this indicates a bug in Bun. Please file a GitHub issue.");
|
||||
}
|
||||
|
||||
this.currently_executing = null;
|
||||
var exec = &this.state.exec;
|
||||
exec.last_exit_code = exit_code;
|
||||
|
||||
|
||||
@@ -260,6 +260,26 @@ pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
pub fn kill(this: *Pipeline, signal: i32) void {
|
||||
log("Pipeline(0x{x}) kill sig={d}", .{ @intFromPtr(this), signal });
|
||||
if (this.cmds) |cmds| {
|
||||
for (cmds) |*cmd_or_result| {
|
||||
if (cmd_or_result.* == .cmd) {
|
||||
const cmd = cmd_or_result.cmd;
|
||||
if (cmd.is(Cmd)) {
|
||||
cmd.as(Cmd).kill(signal);
|
||||
} else if (cmd.is(If)) {
|
||||
cmd.as(If).kill(signal);
|
||||
} else if (cmd.is(CondExpr)) {
|
||||
cmd.as(CondExpr).kill(signal);
|
||||
} else if (cmd.is(Subshell)) {
|
||||
cmd.as(Subshell).kill(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Pipeline) void {
|
||||
// If commands was zero then we didn't allocate anything
|
||||
if (this.cmds == null) return;
|
||||
|
||||
@@ -7,6 +7,7 @@ base: State,
|
||||
node: *const ast.Script,
|
||||
io: IO,
|
||||
parent: ParentPtr,
|
||||
currently_executing: ?*Stmt = null,
|
||||
state: union(enum) {
|
||||
normal: struct {
|
||||
idx: usize = 0,
|
||||
@@ -69,6 +70,7 @@ pub fn next(this: *Script) Yield {
|
||||
this.state.normal.idx += 1;
|
||||
var io = this.getIO();
|
||||
var stmt = Stmt.init(this.base.interpreter, this.base.shell, stmt_node, this, io.ref().*);
|
||||
this.currently_executing = stmt;
|
||||
return stmt.start();
|
||||
},
|
||||
}
|
||||
@@ -85,12 +87,20 @@ fn finish(this: *Script, exit_code: ExitCode) Yield {
|
||||
|
||||
pub fn childDone(this: *Script, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
child.deinit();
|
||||
this.currently_executing = null;
|
||||
if (this.state.normal.idx >= this.node.stmts.len) {
|
||||
return this.finish(exit_code);
|
||||
}
|
||||
return this.next();
|
||||
}
|
||||
|
||||
pub fn kill(this: *Script, signal: i32) void {
|
||||
log("Script(0x{x}) kill sig={d}", .{ @intFromPtr(this), signal });
|
||||
if (this.currently_executing) |stmt| {
|
||||
stmt.kill(signal);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Script) void {
|
||||
log("Script(0x{x}) deinit", .{@intFromPtr(this)});
|
||||
this.io.deref();
|
||||
|
||||
@@ -124,6 +124,27 @@ pub fn childDone(this: *Stmt, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return this.next();
|
||||
}
|
||||
|
||||
pub fn kill(this: *Stmt, signal: i32) void {
|
||||
log("Stmt(0x{x}) kill sig={d}", .{ @intFromPtr(this), signal });
|
||||
if (this.currently_executing) |child| {
|
||||
if (child.ptr.is(Cmd)) {
|
||||
child.as(Cmd).kill(signal);
|
||||
} else if (child.ptr.is(Pipeline)) {
|
||||
child.as(Pipeline).kill(signal);
|
||||
} else if (child.ptr.is(Binary)) {
|
||||
child.as(Binary).kill(signal);
|
||||
} else if (child.ptr.is(Async)) {
|
||||
child.as(Async).kill(signal);
|
||||
} else if (child.ptr.is(Subshell)) {
|
||||
child.as(Subshell).kill(signal);
|
||||
} else if (child.ptr.is(If)) {
|
||||
child.as(If).kill(signal);
|
||||
} else if (child.ptr.is(CondExpr)) {
|
||||
child.as(CondExpr).kill(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Stmt) void {
|
||||
log("Stmt(0x{x}) deinit", .{@intFromPtr(this)});
|
||||
this.io.deinit();
|
||||
|
||||
@@ -4,6 +4,7 @@ base: State,
|
||||
node: *const ast.Subshell,
|
||||
parent: ParentPtr,
|
||||
io: IO,
|
||||
currently_executing: ?*Script = null,
|
||||
state: union(enum) {
|
||||
idle,
|
||||
expanding_redirect: struct {
|
||||
@@ -80,6 +81,7 @@ pub fn initDupeShellState(
|
||||
pub fn start(this: *Subshell) Yield {
|
||||
log("{} start", .{this});
|
||||
const script = Script.init(this.base.interpreter, this.base.shell, &this.node.script, Script.ParentPtr.init(this), this.io.copy());
|
||||
this.currently_executing = script;
|
||||
return script.start();
|
||||
}
|
||||
|
||||
@@ -132,6 +134,7 @@ pub fn next(this: *Subshell) Yield {
|
||||
pub fn transitionToExec(this: *Subshell) Yield {
|
||||
log("{} transitionToExec", .{this});
|
||||
const script = Script.init(this.base.interpreter, this.base.shell, &this.node.script, Script.ParentPtr.init(this), this.io.copy());
|
||||
this.currently_executing = script;
|
||||
this.state = .exec;
|
||||
return script.start();
|
||||
}
|
||||
@@ -150,6 +153,7 @@ pub fn childDone(this: *Subshell, child_ptr: ChildPtr, exit_code: ExitCode) Yiel
|
||||
}
|
||||
|
||||
if (child_ptr.ptr.is(Script)) {
|
||||
this.currently_executing = null;
|
||||
child_ptr.deinit();
|
||||
return this.parent.childDone(this, exit_code);
|
||||
}
|
||||
@@ -170,6 +174,13 @@ pub fn onIOWriterChunk(this: *Subshell, _: usize, err: ?jsc.SystemError) Yield {
|
||||
return this.parent.childDone(this, this.exit_code);
|
||||
}
|
||||
|
||||
pub fn kill(this: *Subshell, signal: i32) void {
|
||||
log("{} kill sig={d}", .{ this, signal });
|
||||
if (this.currently_executing) |script| {
|
||||
script.kill(signal);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Subshell) void {
|
||||
this.base.shell.deinit();
|
||||
this.io.deref();
|
||||
|
||||
378
test/js/bun/shell/shell-kill-edge-cases.test.ts
Normal file
378
test/js/bun/shell/shell-kill-edge-cases.test.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { $ } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Shell kill() - State Node Coverage", () => {
|
||||
test("kill If statement with active condition", async () => {
|
||||
const p = new $.Shell()`if sleep 10; then echo "never"; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill If statement with active then branch", async () => {
|
||||
const p = new $.Shell()`if true; then sleep 10; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill If statement with active else branch", async () => {
|
||||
const p = new $.Shell()`if false; then echo "skip"; else sleep 10; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill If statement with active elif condition", async () => {
|
||||
const p = new $.Shell()`if false; then echo "skip"; elif sleep 10; then echo "never"; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill subshell", async () => {
|
||||
const p = new $.Shell()`(sleep 10)`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill nested subshells", async () => {
|
||||
const p = new $.Shell()`(((sleep 10)))`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill binary AND during first command", async () => {
|
||||
const p = new $.Shell()`sleep 10 && echo "never"`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill binary AND during second command", async () => {
|
||||
const p = new $.Shell()`true && sleep 10`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill binary OR during first command", async () => {
|
||||
const p = new $.Shell()`sleep 10 || echo "never"`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill binary OR during second command", async () => {
|
||||
const p = new $.Shell()`false || sleep 10`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
// Background commands (&) not yet supported
|
||||
// test("kill async command with &", async () => {
|
||||
// const p = new $.Shell()`sleep 10 &`;
|
||||
// await Bun.sleep(50);
|
||||
// p.kill();
|
||||
// const r = await p;
|
||||
// expect(r.exitCode).toBe(137);
|
||||
// });
|
||||
|
||||
test("kill command substitution during expansion", async () => {
|
||||
const p = new $.Shell()`echo $(sleep 10)`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill during glob expansion", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
// Create many files to slow down glob
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
writeFileSync(join(tmpDir, `file${i}.txt`), "");
|
||||
}
|
||||
|
||||
const p = new $.Shell()`echo ${tmpDir}/*.txt`.cwd(tmpDir);
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shell kill() - Race Conditions", () => {
|
||||
test("kill immediately after creation", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill during shell start", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
const promise = p.then(r => r);
|
||||
// Kill during the brief moment of startup
|
||||
p.kill();
|
||||
const r = await promise;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("double kill before start", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
p.kill(); // Should be no-op
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill after completion", async () => {
|
||||
const p = new $.Shell()`true`;
|
||||
const r1 = await p;
|
||||
expect(r1.exitCode).toBe(0);
|
||||
|
||||
// Kill after completion should be no-op
|
||||
p.kill();
|
||||
|
||||
// Result should not change
|
||||
expect(r1.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("rapid sequential kills with different signals", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill(15); // SIGTERM (first kill wins)
|
||||
p.kill(2); // SIGINT (should be ignored - already killed)
|
||||
p.kill(9); // SIGKILL (should be ignored - already killed)
|
||||
const r = await p;
|
||||
// Should use the first signal (SIGTERM)
|
||||
expect(r.exitCode).toBe(143);
|
||||
});
|
||||
|
||||
test("kill racing with builtin completion", async () => {
|
||||
// Echo completes very quickly, try to catch race condition
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const p = new $.Shell()`echo "test"`;
|
||||
const promise = p.then(r => r);
|
||||
p.kill();
|
||||
const r = await promise;
|
||||
// Either killed (137) or completed (0), but should not crash
|
||||
expect([0, 137]).toContain(r.exitCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shell kill() - Complex Pipelines", () => {
|
||||
test("kill 3-stage pipeline", async () => {
|
||||
const p = new $.Shell()`sleep 10 | sleep 10 | sleep 10`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill pipeline with builtins and processes", async () => {
|
||||
const p = new $.Shell()`echo "test" | cat | sleep 10`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill nested pipeline in subshell", async () => {
|
||||
const p = new $.Shell()`(sleep 10 | cat)`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill complex nested structure", async () => {
|
||||
const p = new $.Shell()`if true; then (sleep 10 | cat) && echo "never"; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shell kill() - Builtin Commands", () => {
|
||||
test("kill during ls builtin", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
// Create many files
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
writeFileSync(join(tmpDir, `file${i}.txt`), "");
|
||||
}
|
||||
|
||||
const p = new $.Shell()`ls`.cwd(tmpDir);
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill during cat builtin", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const bigFile = join(tmpDir, "big.txt");
|
||||
writeFileSync(bigFile, "x".repeat(1000000));
|
||||
|
||||
const p = new $.Shell()`cat ${bigFile}`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shell kill() - Signal Variations", () => {
|
||||
test("kill with SIGINT (2)", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill(2);
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(130); // 128 + 2
|
||||
});
|
||||
|
||||
test("kill with SIGQUIT (3)", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill(3);
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(131); // 128 + 3
|
||||
});
|
||||
|
||||
test("kill with SIGHUP (1)", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill(1);
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(129); // 128 + 1
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shell kill() - Resource Management", () => {
|
||||
test("kill does not leak file descriptors", async () => {
|
||||
// Create and kill many shells
|
||||
const promises = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const p = new $.Shell()`sleep 10 | cat | cat`;
|
||||
p.kill();
|
||||
promises.push(p);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (const r of results) {
|
||||
expect(r.exitCode).toBe(137);
|
||||
}
|
||||
|
||||
// If FDs leaked, subsequent operations would fail
|
||||
const p = new $.Shell()`echo "test"`;
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("kill with quiet mode", async () => {
|
||||
const p = new $.Shell()`sleep 10`.quiet();
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill with environment variables", async () => {
|
||||
const p = new $.Shell()`sleep 10`.env({ TEST_VAR: "value" });
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill with custom cwd", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const p = new $.Shell()`sleep 10`.cwd(tmpDir);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shell kill() - Stress Tests", () => {
|
||||
test("rapid kill and await cycle", async () => {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
}
|
||||
});
|
||||
|
||||
test("many concurrent kills", async () => {
|
||||
const promises = Array.from({ length: 50 }, () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
return p;
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (const r of results) {
|
||||
expect(r.exitCode).toBe(137);
|
||||
}
|
||||
});
|
||||
|
||||
test("kill with alternating signals", async () => {
|
||||
const signals = [9, 15, 2, 3, 1, 9, 15, 2, 3, 1];
|
||||
const promises = signals.map(sig => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill(sig);
|
||||
return p.then(r => ({ sig, exitCode: r.exitCode }));
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (const { sig, exitCode } of results) {
|
||||
expect(exitCode).toBe(128 + sig);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shell kill() - Integration with Redirects", () => {
|
||||
test("kill with stdout redirect", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const outFile = join(tmpDir, "out.txt");
|
||||
const p = new $.Shell()`sleep 10 > ${outFile}`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill with stdin redirect", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const inFile = join(tmpDir, "in.txt");
|
||||
writeFileSync(inFile, "test data");
|
||||
const p = new $.Shell()`sleep 10 < ${inFile}`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill with stderr redirect", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const errFile = join(tmpDir, "err.txt");
|
||||
const p = new $.Shell()`sleep 10 2> ${errFile}`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
});
|
||||
268
test/js/bun/shell/shell-kill-safety.test.ts
Normal file
268
test/js/bun/shell/shell-kill-safety.test.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { $ } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
describe("Shell kill() - Safety and Correctness", () => {
|
||||
test("killed shell does not execute subsequent commands", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const marker = join(tmpDir, "should-not-exist.txt");
|
||||
|
||||
const p = new $.Shell()`sleep 1 && echo "bad" > ${marker}`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
await p;
|
||||
|
||||
// The file should not be created because the shell was killed
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
});
|
||||
|
||||
test("killed shell in if-then does not execute then branch", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const marker = join(tmpDir, "should-not-exist.txt");
|
||||
|
||||
const p = new $.Shell()`if sleep 1; then echo "bad" > ${marker}; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
await p;
|
||||
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
});
|
||||
|
||||
test("killed shell in if-else does not execute else branch", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const marker = join(tmpDir, "should-not-exist.txt");
|
||||
|
||||
const p = new $.Shell()`if false; then echo "skip"; else sleep 1 && echo "bad" > ${marker}; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
await p;
|
||||
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
});
|
||||
|
||||
test("killed shell does not leave zombie processes", async () => {
|
||||
// Skip on Windows - ps aux is Unix-only
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create multiple shells that spawn subprocesses
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const p = new $.Shell()`sleep 100 | sleep 100 | sleep 100`;
|
||||
await Bun.sleep(10);
|
||||
p.kill();
|
||||
promises.push(p);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Wait a bit for processes to clean up
|
||||
await Bun.sleep(100);
|
||||
|
||||
// Check that no sleep processes are still running
|
||||
// Note: This is a best-effort check - in production, the OS will clean up orphans
|
||||
const psResult = await $`ps aux`.text();
|
||||
const sleepCount = (psResult.match(/sleep 100/g) || []).length;
|
||||
|
||||
// Should be 0 or very low (might catch some from other tests)
|
||||
expect(sleepCount).toBeLessThan(5);
|
||||
});
|
||||
|
||||
test("kill during file write does not corrupt file", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const outFile = join(tmpDir, "output.txt");
|
||||
|
||||
// Write a known good value first
|
||||
writeFileSync(outFile, "initial content\n");
|
||||
|
||||
// Try to kill during a write operation
|
||||
const p = new $.Shell()`echo "test data" >> ${outFile}`;
|
||||
p.kill();
|
||||
await p;
|
||||
|
||||
// File should either have the initial content or the appended content,
|
||||
// but should not be corrupted or truncated
|
||||
const content = readFileSync(outFile, "utf8");
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
expect(content).toMatch(/^initial content/);
|
||||
});
|
||||
|
||||
test("kill does not affect other concurrent shells", async () => {
|
||||
const p1 = new $.Shell()`sleep 10`;
|
||||
const p2 = new $.Shell()`echo "success"`;
|
||||
const p3 = new $.Shell()`sleep 10`;
|
||||
|
||||
// Kill p1 and p3 but let p2 complete
|
||||
p1.kill();
|
||||
p3.kill();
|
||||
|
||||
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
|
||||
|
||||
expect(r1.exitCode).toBe(137);
|
||||
expect(r2.exitCode).toBe(0); // p2 should complete successfully
|
||||
expect(r2.stdout.toString().trim()).toBe("success");
|
||||
expect(r3.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("multiple awaits on killed shell return same result", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
|
||||
const r1 = await p;
|
||||
const r2 = await p;
|
||||
|
||||
expect(r1.exitCode).toBe(137);
|
||||
expect(r2.exitCode).toBe(137);
|
||||
expect(r1).toBe(r2); // Should be the same object
|
||||
});
|
||||
|
||||
test("kill with invalid signal defaults to SIGKILL", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
|
||||
// Invalid signals (negative, 0, >31) default to SIGKILL (9)
|
||||
p.kill(-1);
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137); // 128 + 9
|
||||
});
|
||||
|
||||
test("kill during pipeline setup does not leak file descriptors", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
|
||||
// Create many files to ensure glob is slow
|
||||
for (let i = 0; i < 100; i++) {
|
||||
writeFileSync(join(tmpDir, `file${i}.txt`), "test");
|
||||
}
|
||||
|
||||
// Try to kill during pipeline setup (during glob expansion)
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const p = new $.Shell()`cat ${tmpDir}/*.txt | wc -l`.cwd(tmpDir);
|
||||
// Kill immediately, might catch during setup
|
||||
p.kill();
|
||||
await p;
|
||||
}
|
||||
|
||||
// If FDs leaked, subsequent file operations would fail
|
||||
const p = new $.Shell()`echo "test"`;
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("kill honors quiet mode (no stderr output)", async () => {
|
||||
const p = new $.Shell()`sleep 10`.quiet();
|
||||
p.kill();
|
||||
const r = await p;
|
||||
|
||||
expect(r.exitCode).toBe(137);
|
||||
// In quiet mode, stderr should be empty
|
||||
expect(r.stderr.length).toBe(0);
|
||||
});
|
||||
|
||||
test("killed shell can be safely garbage collected", async () => {
|
||||
// Create and kill many shells without holding references
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
await p;
|
||||
// Let p go out of scope
|
||||
}
|
||||
|
||||
// Force GC if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
await Bun.sleep(100);
|
||||
global.gc();
|
||||
}
|
||||
|
||||
// System should still be stable
|
||||
const p = new $.Shell()`echo "stable"`;
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(0);
|
||||
expect(r.stdout.toString().trim()).toBe("stable");
|
||||
});
|
||||
|
||||
test("kill before any state transition", async () => {
|
||||
// Kill before the shell even starts parsing
|
||||
const p = new $.Shell()`echo "test" | cat | wc -l`;
|
||||
p.kill(); // Immediate kill
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill returns immediately even if processes take time to die", async () => {
|
||||
const p = new $.Shell()`sleep 100`;
|
||||
const start = Date.now();
|
||||
|
||||
p.kill();
|
||||
const r = await p;
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(r.exitCode).toBe(137);
|
||||
// Should complete in under 1 second (kill should be fast)
|
||||
expect(elapsed).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
test("killed shell with command substitution", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const marker = join(tmpDir, "should-not-exist.txt");
|
||||
|
||||
const p = new $.Shell()`echo $(sleep 1 && echo "bad" > ${marker})`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
await p;
|
||||
|
||||
// The command substitution should be killed
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
});
|
||||
|
||||
test("kill complex nested structure does not execute inner commands", async () => {
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), "shell-test-"));
|
||||
const marker = join(tmpDir, "should-not-exist.txt");
|
||||
|
||||
const p = new $.Shell()`if true; then (sleep 1 && echo "bad" > ${marker}) && echo "never"; fi`;
|
||||
await Bun.sleep(50);
|
||||
p.kill();
|
||||
await p;
|
||||
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
});
|
||||
|
||||
test("kill with very short sleep still returns killed exit code", async () => {
|
||||
// This tests the race between natural completion and kill
|
||||
const results = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const p = new $.Shell()`sleep 0.001`;
|
||||
p.kill();
|
||||
const r = await p;
|
||||
results.push(r.exitCode);
|
||||
}
|
||||
|
||||
// Most should be killed (137), but some might complete naturally (0)
|
||||
const killedCount = results.filter(c => c === 137).length;
|
||||
const completedCount = results.filter(c => c === 0).length;
|
||||
|
||||
// At least some should have been killed
|
||||
expect(killedCount).toBeGreaterThan(0);
|
||||
// Total should be 20
|
||||
expect(killedCount + completedCount).toBe(20);
|
||||
});
|
||||
|
||||
test("kill after shell completes does not cause use-after-free", async () => {
|
||||
// This test caught a critical use-after-free bug where kill() after
|
||||
// completion would access freed Script memory
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const p = new $.Shell()`true`;
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(0);
|
||||
|
||||
// Kill after completion should be safe (no-op)
|
||||
p.kill(); // This used to cause use-after-free!
|
||||
|
||||
// Should still be able to access result
|
||||
expect(r.exitCode).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
94
test/js/bun/shell/shell-kill.test.ts
Normal file
94
test/js/bun/shell/shell-kill.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { $ } from "bun";
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("kill() with default signal (SIGKILL)", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137); // 128 + 9
|
||||
});
|
||||
|
||||
test("kill() with SIGTERM", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill(15);
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(143); // 128 + 15
|
||||
});
|
||||
|
||||
test("kill() before shell starts (lazy execution)", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill() after shell starts", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
const promise = p.then(r => r);
|
||||
await Bun.sleep(100);
|
||||
p.kill();
|
||||
const r = await promise;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill() pipeline", async () => {
|
||||
const p = new $.Shell()`sleep 10 | sleep 10`;
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill() multiple concurrent shells", async () => {
|
||||
const p1 = new $.Shell()`sleep 10`;
|
||||
const p2 = new $.Shell()`sleep 10`;
|
||||
const p3 = new $.Shell()`sleep 10`;
|
||||
|
||||
p1.kill(9);
|
||||
p2.kill(15);
|
||||
p3.kill();
|
||||
|
||||
const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
|
||||
|
||||
expect(r1.exitCode).toBe(137);
|
||||
expect(r2.exitCode).toBe(143);
|
||||
expect(r3.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill() with nothrow()", async () => {
|
||||
const p = new $.Shell()`sleep 10`.nothrow();
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill() builtin command", async () => {
|
||||
const p = new $.Shell()`echo "test" && sleep 10`;
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill() is idempotent", async () => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill();
|
||||
p.kill();
|
||||
p.kill();
|
||||
const r = await p;
|
||||
expect(r.exitCode).toBe(137);
|
||||
});
|
||||
|
||||
test("kill() with different signals", async () => {
|
||||
const signals = [9, 15, 2, 3];
|
||||
const promises = signals.map(async sig => {
|
||||
const p = new $.Shell()`sleep 10`;
|
||||
p.kill(sig);
|
||||
const r = await p;
|
||||
return { sig, exitCode: r.exitCode };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
for (const { sig, exitCode } of results) {
|
||||
expect(exitCode).toBe(128 + sig);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user