feat(shell): add AbortSignal support via .signal() method #25480
+557
−0
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds AbortSignal support to the Bun shell API (
$) via a new.signal()fluent method, enabling cancellation and timeout support for shell commands.Closes #18247
Motivation
The Bun shell API (
$) exposes a high-level, ergonomic interface for spawning pipelines of shell commands. However, it previously lacked the ability to integrate withAbortSignal, making it difficult to:Lower-level APIs like
Bun.spawnalready supportAbortSignal, but the$shell layer did not—until now.API
Behavioral Semantics
Default behavior (throwing)
When the signal aborts, the
ShellPromiserejects with anAbortError(DOMException), matching the behavior offetchandBun.spawn:With
.nothrow()The command resolves normally with exit code 143 (128 + SIGTERM):
Custom abort reasons
Custom abort reasons are preserved:
Already-aborted signals
Passing an already-aborted signal rejects immediately without spawning any processes.
Pipelines
Aborting kills all processes in the pipeline (
cmd1 | cmd2 | cmd3).Helper methods
Works with
.text(),.lines(),.json()- they propagate the abort rejection.Implementation Details
JavaScript Layer (
src/js/builtins/shell.ts)#signal,#abortedByUs, and#alreadyAbortedprivate fields toShellPromise.signal()method that:#throwIfRunning())#abortedByUsflagsetSignal()AbortErrorwhen appropriateZig Layer (
src/shell/interpreter.zig)abort_signalfield andabortedflag toInterpreterstructactive_subprocessesregistry (usingArrayListUnmanaged)registerSubprocess()/unregisterSubprocess()methodsonAbortSignal()callback that:abortedflagabortAllCommands()to SIGTERM all registered subprocessesisAborted()check used by builtins and spawn pointsSubprocess Management (
src/shell/states/Cmd.zig)deinit()Async Commands (
src/shell/states/Async.zig)Builtin Cooperative Cancellation
Added
isAborted()checks at yield points in long-running builtins:yes.zig- in the infinite write loopcat.zig- in the file read loopcp.zig- before copy tasksseq.zig- in the number generation loopType Definitions (
packages/bun-types/shell.d.ts).signal()method with full JSDoc documentationTest Plan
Added comprehensive test suite in
test/js/bun/shell/shell-abort.test.ts(14 tests):Basic abort tests
AbortController.abort()rejects withAbortErrorAbortSignal.timeout()rejects withTimeoutError.nothrow()resolves with exit code 143 on abortAlready-aborted signal tests
.nothrow()resolves with exit code 143Pipeline tests
Helper method tests
.text()rejects on abort.lines()rejects on abort.json()rejects on abortEdge case tests
.signal()after shell has started throwsPromise combinator tests
Promise.race()does not cancel shell command (preserves JS semantics)AbortError properties tests
AbortErrorhas correctnameproperty and is instance ofDOMExceptionFiles Changed
packages/bun-types/shell.d.ts.signal()src/bun.js/api/ParsedShellScript.classes.tssetSignalbindingsrc/js/builtins/shell.tssrc/shell/ParsedShellScript.zigsrc/shell/interpreter.zigsrc/shell/states/Async.zigsrc/shell/states/Cmd.zigsrc/shell/builtin/yes.zigsrc/shell/builtin/cat.zigsrc/shell/builtin/cp.zigsrc/shell/builtin/seq.zigtest/js/bun/shell/shell-abort.test.ts🤖 Generated with Claude Code