mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Compare commits
1 Commits
bun-v1.3.5
...
cursor/imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84feb4b44 |
@@ -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
|
||||
@@ -169,9 +170,9 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
|
||||
if (!this.#hasRun) {
|
||||
this.#hasRun = true;
|
||||
|
||||
let interp = createShellInterpreter(this.#resolve, this.#reject, this.#args!);
|
||||
this.#interpreter = createShellInterpreter(this.#resolve, this.#reject, this.#args!);
|
||||
this.#args = undefined;
|
||||
interp.run();
|
||||
this.#interpreter.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +196,41 @@ export function createBunShellTemplateFunction(createShellInterpreter_, createPa
|
||||
return this;
|
||||
}
|
||||
|
||||
signal(signal: AbortSignal): this {
|
||||
if (signal.aborted) {
|
||||
// If already aborted, cancel immediately
|
||||
if (this.#hasRun && this.#interpreter) {
|
||||
this.#interpreter.cancel();
|
||||
} else if (this.#args) {
|
||||
// If not yet run, we need to cancel once it starts
|
||||
const origRun = this.#run.bind(this);
|
||||
this.#run = () => {
|
||||
origRun();
|
||||
if (this.#interpreter) {
|
||||
this.#interpreter.cancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Listen for abort event
|
||||
signal.addEventListener('abort', () => {
|
||||
if (this.#hasRun && this.#interpreter) {
|
||||
this.#interpreter.cancel();
|
||||
} else if (this.#args) {
|
||||
// If not yet run, we need to cancel once it starts
|
||||
const origRun = this.#run.bind(this);
|
||||
this.#run = () => {
|
||||
origRun();
|
||||
if (this.#interpreter) {
|
||||
this.#interpreter.cancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
async text(encoding) {
|
||||
const { stdout } = (await this.#quiet()) as ShellOutput;
|
||||
return stdout.toString(encoding);
|
||||
|
||||
@@ -626,6 +626,16 @@ pub fn start(this: *Builtin) Yield {
|
||||
return this.callImpl(Yield, "start", .{});
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Builtin) void {
|
||||
// Call the specific builtin's cancel method if it exists
|
||||
this.callImpl(void, "cancel", .{});
|
||||
|
||||
// Set exit code to CANCELLED_EXIT_CODE if not already set
|
||||
if (this.exit_code == null) {
|
||||
this.exit_code = CANCELLED_EXIT_CODE;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Builtin) void {
|
||||
this.callImpl(void, "deinit", .{});
|
||||
|
||||
@@ -771,6 +781,7 @@ const Builtin = Interpreter.Builtin;
|
||||
const JSC = bun.JSC;
|
||||
const Maybe = bun.sys.Maybe;
|
||||
const ExitCode = shell.interpret.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = shell.interpret.CANCELLED_EXIT_CODE;
|
||||
const EnvMap = shell.interpret.EnvMap;
|
||||
const log = shell.interpret.log;
|
||||
const Syscall = bun.sys;
|
||||
|
||||
@@ -99,19 +99,63 @@ pub const Yield = union(enum) {
|
||||
// Note that we're using labelled switch statements but _not_
|
||||
// re-assigning `this`, so the `this` variable is stale after the first
|
||||
// execution. Don't touch it.
|
||||
state: switch (this) {
|
||||
var current_yield = this;
|
||||
state: switch (current_yield) {
|
||||
.pipeline => |x| {
|
||||
pipeline_stack.append(x) catch bun.outOfMemory();
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.cmd => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.script => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.stmt => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.assigns => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.expansion => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.@"if" => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.subshell => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.cond_expr => |x| {
|
||||
if (x.base.interpreter.is_cancelled.load(.monotonic)) {
|
||||
continue :state x.cancel();
|
||||
}
|
||||
continue :state x.next();
|
||||
},
|
||||
.cmd => |x| continue :state x.next(),
|
||||
.script => |x| continue :state x.next(),
|
||||
.stmt => |x| continue :state x.next(),
|
||||
.assigns => |x| continue :state x.next(),
|
||||
.expansion => |x| continue :state x.next(),
|
||||
.@"if" => |x| continue :state x.next(),
|
||||
.subshell => |x| continue :state x.next(),
|
||||
.cond_expr => |x| continue :state x.next(),
|
||||
.on_io_writer_chunk => |x| {
|
||||
const child = IOWriterChildPtr.fromAnyOpaque(x.child);
|
||||
continue :state child.onIOWriterChunk(x.written, x.err);
|
||||
|
||||
@@ -134,6 +134,7 @@ pub const OutputNeedsIOSafeGuard = enum(u0) { output_needs_io };
|
||||
pub const CallstackGuard = enum(u0) { __i_know_what_i_am_doing };
|
||||
|
||||
pub const ExitCode = u16;
|
||||
pub const CANCELLED_EXIT_CODE: ExitCode = 130; // 128 + SIGINT
|
||||
|
||||
pub const StateKind = enum(u8) {
|
||||
script,
|
||||
@@ -282,6 +283,7 @@ pub const Interpreter = struct {
|
||||
|
||||
has_pending_activity: std.atomic.Value(u32) = std.atomic.Value(u32).init(0),
|
||||
started: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
|
||||
is_cancelled: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
|
||||
// Necessary for builtin commands.
|
||||
keep_alive: bun.Async.KeepAlive = .{},
|
||||
|
||||
@@ -1166,21 +1168,33 @@ pub const Interpreter = struct {
|
||||
this.exit_code = exit_code;
|
||||
const this_jsvalue = this.this_jsvalue;
|
||||
if (this_jsvalue != .zero) {
|
||||
if (JSC.Codegen.JSShellInterpreter.resolveGetCached(this_jsvalue)) |resolve| {
|
||||
const loop = this.event_loop.js;
|
||||
const globalThis = this.globalThis;
|
||||
this.this_jsvalue = .zero;
|
||||
this.keep_alive.disable();
|
||||
loop.enter();
|
||||
_ = resolve.call(globalThis, .js_undefined, &.{
|
||||
JSValue.jsNumberFromU16(exit_code),
|
||||
this.getBufferedStdout(globalThis),
|
||||
this.getBufferedStderr(globalThis),
|
||||
}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err);
|
||||
JSC.Codegen.JSShellInterpreter.resolveSetCached(this_jsvalue, globalThis, .js_undefined);
|
||||
JSC.Codegen.JSShellInterpreter.rejectSetCached(this_jsvalue, globalThis, .js_undefined);
|
||||
loop.exit();
|
||||
const loop = this.event_loop.js;
|
||||
const globalThis = this.globalThis;
|
||||
this.this_jsvalue = .zero;
|
||||
this.keep_alive.disable();
|
||||
loop.enter();
|
||||
|
||||
// Handle cancellation by rejecting with AbortError
|
||||
if (exit_code == CANCELLED_EXIT_CODE) {
|
||||
if (JSC.Codegen.JSShellInterpreter.rejectGetCached(this_jsvalue)) |reject| {
|
||||
// Create a DOMException with name "AbortError"
|
||||
const error_str = bun.String.static("The operation was aborted");
|
||||
const abort_error = JSC.DOMException.createAbortError(globalThis, &error_str);
|
||||
_ = reject.call(globalThis, .js_undefined, &.{abort_error}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err);
|
||||
}
|
||||
} else {
|
||||
if (JSC.Codegen.JSShellInterpreter.resolveGetCached(this_jsvalue)) |resolve| {
|
||||
_ = resolve.call(globalThis, .js_undefined, &.{
|
||||
JSValue.jsNumberFromU16(exit_code),
|
||||
this.getBufferedStdout(globalThis),
|
||||
this.getBufferedStderr(globalThis),
|
||||
}) catch |err| globalThis.reportActiveExceptionAsUnhandled(err);
|
||||
}
|
||||
}
|
||||
|
||||
JSC.Codegen.JSShellInterpreter.resolveSetCached(this_jsvalue, globalThis, .js_undefined);
|
||||
JSC.Codegen.JSShellInterpreter.rejectSetCached(this_jsvalue, globalThis, .js_undefined);
|
||||
loop.exit();
|
||||
}
|
||||
} else {
|
||||
this.flags.done = true;
|
||||
@@ -1308,6 +1322,11 @@ pub const Interpreter = struct {
|
||||
this.deinitFromFinalizer();
|
||||
}
|
||||
|
||||
pub fn cancel(this: *ThisInterpreter) callconv(.C) void {
|
||||
log("Interpreter(0x{x}) cancel", .{@intFromPtr(this)});
|
||||
this.is_cancelled.store(true, .seq_cst);
|
||||
}
|
||||
|
||||
pub fn hasPendingActivity(this: *ThisInterpreter) bool {
|
||||
return this.has_pending_activity.load(.seq_cst) > 0;
|
||||
}
|
||||
@@ -1479,6 +1498,20 @@ pub fn StatePtrUnion(comptime TypesValue: anytype) type {
|
||||
unknownTag(this.tagInt());
|
||||
}
|
||||
|
||||
/// Cancels the state node
|
||||
pub fn cancel(this: @This()) Yield {
|
||||
const tags = comptime std.meta.fields(Ptr.Tag);
|
||||
inline for (tags) |tag| {
|
||||
if (this.tagInt() == tag.value) {
|
||||
const Ty = comptime Ptr.typeFromTag(tag.value);
|
||||
Ptr.assert_type(Ty);
|
||||
var casted = this.as(Ty);
|
||||
return casted.cancel();
|
||||
}
|
||||
}
|
||||
unknownTag(this.tagInt());
|
||||
}
|
||||
|
||||
pub fn unknownTag(tag: Ptr.TagInt) noreturn {
|
||||
return bun.Output.panic("Unknown tag for shell state node: {d}\n", .{tag});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub const Interpreter = interpret.Interpreter;
|
||||
pub const ParsedShellScript = interpret.ParsedShellScript;
|
||||
pub const Subprocess = subproc.ShellSubprocess;
|
||||
pub const ExitCode = interpret.ExitCode;
|
||||
pub const CANCELLED_EXIT_CODE = interpret.CANCELLED_EXIT_CODE;
|
||||
pub const IOWriter = Interpreter.IOWriter;
|
||||
pub const IOReader = Interpreter.IOReader;
|
||||
// pub const IOWriter = interpret.IOWriter;
|
||||
|
||||
@@ -39,6 +39,18 @@ pub inline fn deinit(this: *Assigns) void {
|
||||
if (this.owned) this.parent.destroy(this);
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Assigns) Yield {
|
||||
log("Assigns(0x{x}) cancel", .{@intFromPtr(this)});
|
||||
|
||||
// Clean up current expansion if any
|
||||
if (this.state == .expanding) {
|
||||
this.state.expanding.current_expansion_result.clearAndFree();
|
||||
}
|
||||
|
||||
// Propagate cancellation to parent
|
||||
return this.parent.childDone(this, CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn start(this: *Assigns) Yield {
|
||||
return .{ .assigns = this };
|
||||
}
|
||||
@@ -220,6 +232,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -131,6 +131,22 @@ pub fn childDone(this: *Async, child_ptr: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Async) Yield {
|
||||
log("{} cancel", .{this});
|
||||
|
||||
// Cancel the child if it's running
|
||||
if (this.state == .exec) {
|
||||
if (this.state.exec.child) |child| {
|
||||
_ = child.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as done with CANCELLED_EXIT_CODE
|
||||
this.state = .{ .done = CANCELLED_EXIT_CODE };
|
||||
this.enqueueSelf();
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
/// This function is purposefully empty as a hack to ensure Async runs in the background while appearing to
|
||||
/// the parent that it is done immediately.
|
||||
///
|
||||
@@ -164,6 +180,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -141,6 +141,18 @@ pub fn childDone(this: *Binary, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return this.parent.childDone(this, exit_code);
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Binary) Yield {
|
||||
log("binary cancel {x} ({s})", .{ @intFromPtr(this), @tagName(this.node.op) });
|
||||
|
||||
// Cancel the currently executing child if any
|
||||
if (this.currently_executing) |child| {
|
||||
return child.cancel();
|
||||
}
|
||||
|
||||
// Propagate cancellation to parent
|
||||
return this.parent.childDone(this, CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Binary) void {
|
||||
if (this.currently_executing) |child| {
|
||||
child.deinit();
|
||||
@@ -157,6 +169,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -694,6 +694,39 @@ pub fn onExit(this: *Cmd, exit_code: ExitCode) void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Cmd) Yield {
|
||||
log("Cmd(0x{x}, {s}) cancel", .{ @intFromPtr(this), @tagName(this.exec) });
|
||||
|
||||
// Set internal state to cancelling
|
||||
if (this.exit_code == null) {
|
||||
this.exit_code = CANCELLED_EXIT_CODE;
|
||||
}
|
||||
|
||||
// Cancel subprocess or builtin
|
||||
if (this.exec == .subproc) {
|
||||
// Send SIGTERM to subprocess
|
||||
_ = this.exec.subproc.child.tryKill(9); // TODO: Use SIGTERM instead of SIGKILL
|
||||
} else if (this.exec == .bltn) {
|
||||
// Cancel builtin
|
||||
this.exec.bltn.cancel();
|
||||
}
|
||||
|
||||
// Cancel chunks in IOWriter for stdout/stderr if they are file descriptors
|
||||
if (this.io.stdout == .fd) {
|
||||
if (this.io.stdout.fd.writer) |writer| {
|
||||
writer.cancelChunks(this);
|
||||
}
|
||||
}
|
||||
if (this.io.stderr == .fd) {
|
||||
if (this.io.stderr.fd.writer) |writer| {
|
||||
writer.cancelChunks(this);
|
||||
}
|
||||
}
|
||||
|
||||
// State machine will handle cleanup via normal childDone pathway
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
// TODO check that this also makes sure that the poll ref is killed because if it isn't then this Cmd pointer will be stale and so when the event for pid exit happens it will cause crash
|
||||
pub fn deinit(this: *Cmd) void {
|
||||
log("Cmd(0x{x}, {s}) cmd deinit", .{ @intFromPtr(this), @tagName(this.exec) });
|
||||
@@ -807,6 +840,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -210,6 +210,13 @@ fn doStat(this: *CondExpr) Yield {
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
pub fn cancel(this: *CondExpr) Yield {
|
||||
log("{} cancel", .{this});
|
||||
|
||||
// Propagate cancellation to parent with CANCELLED_EXIT_CODE
|
||||
return this.parent.childDone(this, CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *CondExpr) void {
|
||||
this.io.deinit();
|
||||
for (this.args.items) |item| {
|
||||
@@ -276,6 +283,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -146,6 +146,24 @@ pub fn init(
|
||||
expansion.current_out = std.ArrayList(u8).init(expansion.base.allocator());
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Expansion) Yield {
|
||||
log("Expansion(0x{x}) cancel", .{@intFromPtr(this)});
|
||||
|
||||
// If a command substitution is running, cancel it
|
||||
if (this.child_state == .cmd_subst) {
|
||||
return this.child_state.cmd_subst.cmd.cancel();
|
||||
}
|
||||
|
||||
// Clean up state
|
||||
this.current_out.deinit();
|
||||
if (this.child_state == .glob) {
|
||||
this.child_state.glob.walker.deinit(true);
|
||||
}
|
||||
|
||||
// Propagate cancellation to parent
|
||||
return this.parent.childDone(this, CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn deinit(expansion: *Expansion) void {
|
||||
log("Expansion(0x{x}) deinit", .{@intFromPtr(expansion)});
|
||||
expansion.current_out.deinit();
|
||||
@@ -857,6 +875,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const GlobWalker = bun.shell.interpret.GlobWalker;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
|
||||
@@ -148,6 +148,13 @@ pub fn next(this: *If) Yield {
|
||||
return this.parent.childDone(this, 0);
|
||||
}
|
||||
|
||||
pub fn cancel(this: *If) Yield {
|
||||
log("{} cancel", .{this});
|
||||
|
||||
// Propagate cancellation to parent with CANCELLED_EXIT_CODE
|
||||
return this.parent.childDone(this, CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *If) void {
|
||||
log("{} deinit", .{this});
|
||||
this.io.deref();
|
||||
@@ -191,6 +198,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -260,6 +260,31 @@ pub fn childDone(this: *Pipeline, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Pipeline) Yield {
|
||||
log("Pipeline(0x{x}) cancel", .{@intFromPtr(this)});
|
||||
|
||||
// First close all pipes to unblock any processes stuck on I/O
|
||||
if (this.pipes) |pipes| {
|
||||
for (pipes) |*pipe| {
|
||||
closefd(pipe[0]);
|
||||
closefd(pipe[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel all running commands
|
||||
if (this.cmds) |cmds| {
|
||||
for (cmds) |cmd_or_result| {
|
||||
if (cmd_or_result == .cmd) {
|
||||
// Cancel the command
|
||||
_ = cmd_or_result.cmd.call("cancel", .{}, Yield);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The pipeline will wait for all children to report childDone before propagating cancellation
|
||||
return .suspended;
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Pipeline) void {
|
||||
// If commands was zero then we didn't allocate anything
|
||||
if (this.cmds == null) return;
|
||||
@@ -333,6 +358,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -91,6 +91,14 @@ pub fn childDone(this: *Script, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return this.next();
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Script) Yield {
|
||||
log("Script(0x{x}) cancel", .{@intFromPtr(this)});
|
||||
|
||||
// Since scripts don't have direct children running in parallel,
|
||||
// we just propagate the cancellation to the parent with CANCELLED_EXIT_CODE
|
||||
return this.finish(CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Script) void {
|
||||
log("Script(0x{x}) deinit", .{@intFromPtr(this)});
|
||||
this.io.deref();
|
||||
@@ -122,6 +130,8 @@ const InterpreterChildPtr = Interpreter.InterpreterChildPtr;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -124,6 +124,18 @@ pub fn childDone(this: *Stmt, child: ChildPtr, exit_code: ExitCode) Yield {
|
||||
return this.next();
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Stmt) Yield {
|
||||
log("Stmt(0x{x}) cancel", .{@intFromPtr(this)});
|
||||
|
||||
// Cancel the currently executing child if any
|
||||
if (this.currently_executing) |child| {
|
||||
return child.cancel();
|
||||
}
|
||||
|
||||
// Otherwise propagate cancellation to parent
|
||||
return this.parent.childDone(this, CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Stmt) void {
|
||||
log("Stmt(0x{x}) deinit", .{@intFromPtr(this)});
|
||||
this.io.deinit();
|
||||
@@ -141,6 +153,8 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
@@ -170,6 +170,13 @@ pub fn onIOWriterChunk(this: *Subshell, _: usize, err: ?JSC.SystemError) Yield {
|
||||
return this.parent.childDone(this, this.exit_code);
|
||||
}
|
||||
|
||||
pub fn cancel(this: *Subshell) Yield {
|
||||
log("{} cancel", .{this});
|
||||
|
||||
// Propagate cancellation to parent with CANCELLED_EXIT_CODE
|
||||
return this.parent.childDone(this, CANCELLED_EXIT_CODE);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Subshell) void {
|
||||
this.base.shell.deinit();
|
||||
this.io.deref();
|
||||
@@ -196,6 +203,7 @@ const Interpreter = bun.shell.Interpreter;
|
||||
const StatePtrUnion = bun.shell.interpret.StatePtrUnion;
|
||||
const ast = bun.shell.AST;
|
||||
const ExitCode = bun.shell.ExitCode;
|
||||
const CANCELLED_EXIT_CODE = bun.shell.CANCELLED_EXIT_CODE;
|
||||
const ShellExecEnv = Interpreter.ShellExecEnv;
|
||||
const State = bun.shell.Interpreter.State;
|
||||
const IO = bun.shell.Interpreter.IO;
|
||||
|
||||
113
test_shell_cancellation.js
Normal file
113
test_shell_cancellation.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const { $ } = require('bun');
|
||||
|
||||
async function testShellCancellation() {
|
||||
console.log('Testing shell cancellation...\n');
|
||||
|
||||
// Test 1: Cancel a simple long-running command
|
||||
{
|
||||
console.log('Test 1: Cancelling sleep command');
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const start = Date.now();
|
||||
const promise = $`sleep 5`.signal(signal);
|
||||
|
||||
// Cancel after 1 second
|
||||
setTimeout(() => {
|
||||
console.log(' Sending abort signal...');
|
||||
controller.abort();
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
await promise;
|
||||
console.log(' ❌ Expected command to be cancelled');
|
||||
} catch (err) {
|
||||
const elapsed = Date.now() - start;
|
||||
if (err.name === 'AbortError') {
|
||||
console.log(` ✅ Command cancelled after ${elapsed}ms (AbortError)`);
|
||||
} else {
|
||||
console.log(` ❌ Unexpected error: ${err.name} - ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: Cancel a pipeline
|
||||
{
|
||||
console.log('\nTest 2: Cancelling pipeline');
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const start = Date.now();
|
||||
const promise = $`yes | head -n 100000`.signal(signal);
|
||||
|
||||
// Cancel after 500ms
|
||||
setTimeout(() => {
|
||||
console.log(' Sending abort signal...');
|
||||
controller.abort();
|
||||
}, 500);
|
||||
|
||||
try {
|
||||
await promise;
|
||||
console.log(' ❌ Expected pipeline to be cancelled');
|
||||
} catch (err) {
|
||||
const elapsed = Date.now() - start;
|
||||
if (err.name === 'AbortError') {
|
||||
console.log(` ✅ Pipeline cancelled after ${elapsed}ms (AbortError)`);
|
||||
} else {
|
||||
console.log(` ❌ Unexpected error: ${err.name} - ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: Pre-aborted signal
|
||||
{
|
||||
console.log('\nTest 3: Pre-aborted signal');
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
await $`sleep 5`.signal(controller.signal);
|
||||
console.log(' ❌ Expected command to be cancelled immediately');
|
||||
} catch (err) {
|
||||
const elapsed = Date.now() - start;
|
||||
if (err.name === 'AbortError' && elapsed < 100) {
|
||||
console.log(` ✅ Command cancelled immediately (${elapsed}ms)`);
|
||||
} else {
|
||||
console.log(` ❌ Unexpected error or timing: ${err.name} - ${elapsed}ms`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 4: Cancel command substitution
|
||||
{
|
||||
console.log('\nTest 4: Cancelling command substitution');
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const start = Date.now();
|
||||
const promise = $`echo $(sleep 5 && echo "done")`.signal(signal);
|
||||
|
||||
// Cancel after 1 second
|
||||
setTimeout(() => {
|
||||
console.log(' Sending abort signal...');
|
||||
controller.abort();
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
await promise;
|
||||
console.log(' ❌ Expected command substitution to be cancelled');
|
||||
} catch (err) {
|
||||
const elapsed = Date.now() - start;
|
||||
if (err.name === 'AbortError') {
|
||||
console.log(` ✅ Command substitution cancelled after ${elapsed}ms`);
|
||||
} else {
|
||||
console.log(` ❌ Unexpected error: ${err.name} - ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nAll tests completed!');
|
||||
}
|
||||
|
||||
testShellCancellation().catch(console.error);
|
||||
Reference in New Issue
Block a user