mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## Summary - Fixed `Bun.Terminal` callbacks (data, exit, drain) not being invoked when the terminal is created inside `AsyncLocalStorage.run()` ## Root Cause The bug was a redundant `isCallable()` check when storing callbacks in `initTerminal()`: 1. In `Options.parseFromJS()`, callbacks are validated with `isCallable()`, then wrapped with `withAsyncContextIfNeeded()` 2. Inside `AsyncLocalStorage.run()`, `withAsyncContextIfNeeded()` returns an `AsyncContextFrame` object that wraps the callback + async context 3. An `AsyncContextFrame` is NOT callable - it's a wrapper object. So the second `isCallable()` check fails 4. Because the check fails, the callback is never stored via `js.gc.set()` 5. When `onReadChunk()` tries to get the callback, it returns `null` and the callback is never invoked ## Fix Removed the redundant `isCallable()` check in `initTerminal()`. The check was already performed in `parseFromJS()` before wrapping. Other similar patterns (socket Handlers, Timer) simply store the wrapped callback without re-checking. Fixes #26286 ## Test plan - [x] Added regression test in `test/regression/issue/26286.test.ts` - [x] Verified test fails with system Bun (times out because callback never invoked) - [x] Verified test passes with debug build 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, isWindows } from "harness";
|
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
|
// https://github.com/oven-sh/bun/issues/26286
|
|
// Bun.Terminal callbacks not invoked inside AsyncLocalStorage.run()
|
|
|
|
// Bun.Terminal uses PTY which is not supported on Windows
|
|
test.skipIf(isWindows)("Bun.Terminal data callback works inside AsyncLocalStorage.run()", async () => {
|
|
const storage = new AsyncLocalStorage();
|
|
|
|
async function terminalTest() {
|
|
const { promise, resolve } = Promise.withResolvers<Uint8Array>();
|
|
|
|
await using terminal = new Bun.Terminal({
|
|
data(term, data) {
|
|
resolve(data);
|
|
},
|
|
});
|
|
|
|
const process = Bun.spawn([bunExe(), "-e", "console.log('Hello')"], {
|
|
terminal,
|
|
env: bunEnv,
|
|
});
|
|
|
|
const data = await promise;
|
|
await process.exited;
|
|
|
|
return { data };
|
|
}
|
|
|
|
// Test inside AsyncLocalStorage.run()
|
|
const result = await storage.run({ testContext: true }, terminalTest);
|
|
|
|
expect(result.data).not.toBeNull();
|
|
expect(new TextDecoder().decode(result.data!)).toContain("Hello");
|
|
});
|
|
|
|
test.skipIf(isWindows)("Bun.Terminal preserves async context inside callbacks", async () => {
|
|
const storage = new AsyncLocalStorage<{ id: number }>();
|
|
|
|
async function terminalTest() {
|
|
const { promise, resolve } = Promise.withResolvers<{ id: number } | undefined>();
|
|
|
|
await using terminal = new Bun.Terminal({
|
|
data(term, data) {
|
|
resolve(storage.getStore());
|
|
},
|
|
});
|
|
|
|
const process = Bun.spawn([bunExe(), "-e", "console.log('Hello')"], {
|
|
terminal,
|
|
env: bunEnv,
|
|
});
|
|
|
|
const contextInCallback = await promise;
|
|
await process.exited;
|
|
|
|
return { contextInCallback };
|
|
}
|
|
|
|
const result = await storage.run({ id: 42 }, terminalTest);
|
|
|
|
expect(result.contextInCallback).not.toBeUndefined();
|
|
expect(result.contextInCallback?.id).toBe(42);
|
|
});
|