Repository navigation
prompt, alert, confirm: run JS signal listeners while waiting for input #41723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
ed6f471
8a92c93
47498f0
2eb7bc5
a23d44a
6ebacfb
45f76fc
12fa655
7571c30
bdaa8ec
22b4245
9b1d3c5
ee828e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,32 @@ impl PosixSignalHandle { | |
| event_loop.enqueue_task(task); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(unix)] | ||
| fn main_thread() -> Option<bun_ptr::BackRef<Self>> { | ||
| let vm = VirtualMachine::get_main_thread_vm()?; | ||
| // SAFETY: `vm` and its event loop are process-lifetime; only the | ||
| // `signal_handler` slot is read. | ||
| unsafe { (*(*vm).event_loop()).signal_handler } | ||
| } | ||
|
|
||
| /// `true` when a signal handler has queued a signal the main thread has not consumed. | ||
| #[cfg(unix)] | ||
| pub fn has_queued() -> bool { | ||
| Self::main_thread().is_some_and(|handler| !handler.ring.is_empty()) | ||
| } | ||
|
|
||
| /// Runs the JS listeners for every queued signal now, for a host function that | ||
| /// blocks the JS thread (`prompt()`). Main thread only, the ring's single consumer. | ||
| #[cfg(unix)] | ||
| pub fn run_queued_from_js_thread(global_object: &JSGlobalObject) { | ||
| let Some(handler) = Self::main_thread() else { | ||
| return; | ||
| }; | ||
| while let Some(signal) = handler.ring.dequeue() { | ||
| PosixSignalTask::run_from_js_thread(signal, global_object); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) Signal listeners now run inside whatever AsyncLocalStorage or Bun.ModuleGraph context the dialog's caller was in, not the event-loop context they get on the base branch. run_queued_from_js_thread at src/jsc/PosixSignalHandle.rs:79 calls the listener while $asyncContext still holds the caller's frame, and nothing saves or resets it. So getStore() in the listener returns the caller's store, and a throw in it is routed to that graph's onError through currentModuleGraph. Fix: run the queued listeners (and the drain at :84) under the event-loop async context, the value drainMicrotasks resets to at ZigGlobalObject.cpp:3128, then restore the caller's frame. Why this was flaggedScript: als.run(store, () => prompt("name?")) with process.on("SIGTERM", () => log(als.getStore())), or the same prompt() call made from a Bun.ModuleGraph script. SIGTERM during the wait reaches wait_for_stdin (src/runtime/webcore/prompt.rs:31) -> run_queued_from_js_thread -> PosixSignalTask::run_from_js_thread (src/jsc/PosixSignalHandle.rs:79) -> innerInvokeEventListeners, a plain JSC::call (src/jsc/bindings/webcore/EventEmitter.cpp:260) with no AsyncContextFrame wrapping. The listener reads $asyncContext field 0, which is still the caller's frame, so getStore() returns store. If the listener throws, Bun__ModuleGraph__handleUncaughtException (src/jsc/VirtualMachine.rs:2105) picks the graph via currentModuleGraph, which reads the same field, so the error goes to that graph's onError instead of the process-wide path. On base the listener ran as a PosixSignalTask with ContextId::NONE after a checkpoint had reset the frame to moduleGraphAsyncContextAtEventLoop (src/jsc/bindings/ZigGlobalObject.cpp:3127-3128), so getStore() was undefined and the throw went to the host. The reset in drainMicrotasks does not help here because vm.entryScope is set while prompt() is on the stack. Verification:
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not changed here. I documented it in the PR (Downsides and notes). The listener runs synchronously inside the dialog's caller, so it sees that caller's |
||
| } | ||
|
robobun marked this conversation as resolved.
Resolved
|
||
| } | ||
|
robobun marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| /// This is the signal handler entry point. Calls enqueue on the ring buffer. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.