Repository navigation
install: forward SIGINT/SIGTERM/SIGHUP to running lifecycle scripts and wait for them #41921
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
Open
robobun
wants to merge
6
commits into
main
Choose a base branch
from
robobun/85a3ecdc/install-forward-signals-to-lifecycle-scripts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
4eab15f
install: forward SIGINT/SIGTERM/SIGHUP to running lifecycle scripts a…
robobun f8556c9
Keep a signal received across a re-hook, enqueue from the handler wit…
robobun cfd7304
Trim comments
robobun 5524f38
Shorter module docs
robobun e820e7a
Die by a signal that lands while the last script's exit is handled
robobun b5465a2
ci: retrigger
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| //! Forwards SIGINT/SIGTERM/SIGHUP sent to `bun install` to its running | ||
| //! lifecycle scripts, waits for them, then dies by the same signal (hook: | ||
| //! `bun_spawn::exit_signals`). After forwarding, a second signal is default. | ||
|
robobun marked this conversation as resolved.
|
||
|
|
||
| use core::ffi::c_int; | ||
| use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; | ||
|
|
||
| use bun_spawn::exit_signals; | ||
|
|
||
| use crate::PackageManager; | ||
| use crate::lifecycle_script_runner::LifecycleScriptSubprocess; | ||
|
|
||
| static RUNNING: AtomicUsize = AtomicUsize::new(0); | ||
| static MANAGER: AtomicPtr<PackageManager> = AtomicPtr::new(core::ptr::null_mut()); | ||
|
|
||
| /// A hooked signal arrived (forwarded or about to be). While `Some`, no | ||
| /// script starts or chains, and the install ends by it after the last script. | ||
|
robobun marked this conversation as resolved.
|
||
| pub(crate) fn pending() -> Option<bun_core::SignalCode> { | ||
| exit_signals::received().map(signal_code) | ||
| } | ||
|
|
||
| fn signal_code(sig: c_int) -> bun_core::SignalCode { | ||
| u8::try_from(sig) | ||
| .ok() | ||
| .and_then(bun_core::SignalCode::from_raw) | ||
| .unwrap_or(bun_core::SignalCode::DEFAULT) | ||
| } | ||
|
|
||
| /// Call before spawning a lifecycle script. `manager` (allocation-rooted) is | ||
| /// dereferenced from the signal callback until the last script exits. | ||
|
robobun marked this conversation as resolved.
|
||
| pub(crate) fn on_script_started(manager: *mut PackageManager) { | ||
| if RUNNING.fetch_add(1, Ordering::Relaxed) != 0 { | ||
| return; | ||
| } | ||
| MANAGER.store(manager, Ordering::Relaxed); | ||
| // SAFETY: `manager` is live; only `event_loop` is borrowed, for this call. | ||
| let event_loop = | ||
| bun_event_loop::EventLoopHandle::from_any(unsafe { &mut (*manager).event_loop }); | ||
| exit_signals::hook(event_loop, on_signal); | ||
| } | ||
|
|
||
| /// Call when a lifecycle script exited or failed to spawn. After the last one | ||
| /// unhooks, then dies by a signal that arrived while hooked, if any. | ||
|
robobun marked this conversation as resolved.
|
||
| pub(crate) fn on_script_exited() { | ||
| if RUNNING.fetch_sub(1, Ordering::Relaxed) != 1 { | ||
| return; | ||
| } | ||
| exit_signals::unhook(); | ||
| MANAGER.store(core::ptr::null_mut(), Ordering::Relaxed); | ||
| // Checked after `unhook` so a signal either shows up here or already took | ||
| // the default action; none is lost in between. | ||
|
robobun marked this conversation as resolved.
|
||
| if let Some(sig) = pending() { | ||
| die(sig); | ||
| } | ||
| } | ||
|
|
||
| fn die(sig: bun_core::SignalCode) -> ! { | ||
| bun_core::Output::flush(); | ||
| bun_core::Global::raise_ignoring_panic_handler(sig); | ||
| } | ||
|
|
||
| /// Install thread, between event loop ticks. | ||
| fn on_signal(sig: c_int) { | ||
| let manager = MANAGER.load(Ordering::Relaxed); | ||
| if manager.is_null() || RUNNING.load(Ordering::Relaxed) == 0 { | ||
| // The last script exited before this ran: nothing to wait for. | ||
| die(signal_code(sig)); | ||
| } | ||
| exit_signals::unhook(); | ||
| let forward = signal_code(sig) as u8; | ||
| // SAFETY: `manager` is live (see `on_script_started`); the heap is only | ||
| // touched on this thread. | ||
| unsafe { | ||
| (*manager).active_lifecycle_scripts.for_each( | ||
| |script: *mut LifecycleScriptSubprocess<'static>| { | ||
| if let Some(process) = &(*script).process | ||
| && !process.process_mut().has_exited() | ||
| { | ||
| let _ = process.kill(forward); | ||
| } | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| //! SIGINT/SIGTERM/SIGHUP hook for a supervisor of background children (`bun | ||
| //! install` running lifecycle scripts): the handler records the signal and | ||
| //! posts the supervisor's callback to its `MiniEventLoop`. Contrast | ||
| //! [`crate::ctrl_c`], where the child shares the terminal and nothing is | ||
| //! forwarded. | ||
|
robobun marked this conversation as resolved.
|
||
|
|
||
| use core::ffi::c_int; | ||
| use core::ptr::NonNull; | ||
| use core::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, Ordering}; | ||
|
|
||
| use bun_core::Mutex; | ||
| use bun_event_loop::AnyTaskWithExtraContext::{AnyTaskWithExtraContext, New}; | ||
| use bun_event_loop::EventLoopHandle; | ||
| use bun_event_loop::MiniEventLoop::MiniEventLoop; | ||
|
|
||
| pub const SIGNALS: [c_int; 3] = [libc::SIGINT, libc::SIGTERM, libc::SIGHUP]; | ||
|
|
||
| /// Last hooked signal received, 0 when none. Never cleared, so a task queued | ||
| /// under one hook still sees it after an unhook/hook cycle. | ||
|
robobun marked this conversation as resolved.
|
||
| static RECEIVED: AtomicI32 = AtomicI32::new(0); | ||
| /// `TASK` is in the loop's queue. `SeqCst` with `RECEIVED`: a handler (any | ||
| /// thread) that sees this set relies on the queued task reading its store. | ||
|
robobun marked this conversation as resolved.
|
||
| static QUEUED: AtomicBool = AtomicBool::new(false); | ||
| static EVENT_LOOP: AtomicPtr<MiniEventLoop> = AtomicPtr::new(core::ptr::null_mut()); | ||
| /// Allocated once, never freed: the handler must not allocate. | ||
| static TASK: AtomicPtr<AnyTaskWithExtraContext> = AtomicPtr::new(core::ptr::null_mut()); | ||
| static ON_SIGNAL: Mutex<Option<fn(c_int)>> = Mutex::new(None); | ||
| /// Dispositions replaced by `hook`; `None` while not hooked. | ||
| static PREVIOUS: Mutex<Option<[libc::sigaction; SIGNALS.len()]>> = Mutex::new(None); | ||
|
|
||
| /// Until [`unhook`], a hooked signal runs `on_signal(sig)` on the loop's | ||
| /// thread at its next tick (coalesced; latest wins). Skips an inherited | ||
| /// `SIG_IGN`. Returns `false` (no hook) for a JS event loop. | ||
|
robobun marked this conversation as resolved.
|
||
| pub fn hook(event_loop: EventLoopHandle, on_signal: fn(c_int)) -> bool { | ||
| let EventLoopHandle::Mini(mini) = event_loop else { | ||
| return false; | ||
| }; | ||
| let mut previous = PREVIOUS.lock(); | ||
| *ON_SIGNAL.lock() = Some(on_signal); | ||
| if previous.is_some() { | ||
| return true; | ||
| } | ||
| if TASK.load(Ordering::Relaxed).is_null() { | ||
| let task = New::<(), ()>::init(NonNull::<()>::dangling().as_ptr(), run_task); | ||
| TASK.store(bun_core::heap::into_raw(Box::new(task)), Ordering::Relaxed); | ||
| } | ||
| // Published before the dispositions so the handler always finds a loop. | ||
| EVENT_LOOP.store(mini.as_ptr(), Ordering::Release); | ||
|
|
||
| let mut saved: [libc::sigaction; SIGNALS.len()] = [bun_core::ffi::zeroed(); SIGNALS.len()]; | ||
| // SAFETY: zeroed `sigaction` is valid; all pointers are to live locals. | ||
| unsafe { | ||
| let mut act: libc::sigaction = bun_core::ffi::zeroed(); | ||
| act.sa_sigaction = handler as *const () as usize; | ||
| act.sa_flags = libc::SA_RESTART; | ||
| libc::sigemptyset(&raw mut act.sa_mask); | ||
| for (i, sig) in SIGNALS.iter().enumerate() { | ||
| if libc::sigaction(*sig, core::ptr::null(), &raw mut saved[i]) != 0 { | ||
| continue; | ||
| } | ||
| if saved[i].sa_sigaction == libc::SIG_IGN { | ||
| continue; | ||
| } | ||
| libc::sigaction(*sig, &raw const act, core::ptr::null_mut()); | ||
| } | ||
| } | ||
| *previous = Some(saved); | ||
| true | ||
| } | ||
|
|
||
| /// Restores the dispositions [`hook`] replaced. [`received`] keeps its value. | ||
| /// No-op when not hooked. | ||
|
robobun marked this conversation as resolved.
|
||
| pub fn unhook() { | ||
| let mut previous = PREVIOUS.lock(); | ||
| let Some(saved) = previous.take() else { | ||
| return; | ||
| }; | ||
| // SAFETY: `saved` holds dispositions the kernel returned in `hook`. | ||
| unsafe { | ||
| for (i, sig) in SIGNALS.iter().enumerate() { | ||
| libc::sigaction(*sig, &raw const saved[i], core::ptr::null_mut()); | ||
| } | ||
| } | ||
| EVENT_LOOP.store(core::ptr::null_mut(), Ordering::Release); | ||
| } | ||
|
|
||
| /// The last hooked signal received, if any (also after [`unhook`]). | ||
| pub fn received() -> Option<c_int> { | ||
| match RECEIVED.load(Ordering::SeqCst) { | ||
| 0 => None, | ||
| sig => Some(sig), | ||
| } | ||
| } | ||
|
|
||
| extern "C" fn handler(sig: c_int) { | ||
| RECEIVED.store(sig, Ordering::SeqCst); | ||
| if QUEUED.swap(true, Ordering::SeqCst) { | ||
| return; | ||
| } | ||
| let event_loop = EVENT_LOOP.load(Ordering::Acquire); | ||
| let task = NonNull::new(TASK.load(Ordering::Relaxed)); | ||
| let (false, Some(task)) = (event_loop.is_null(), task) else { | ||
| // Raced with `unhook` on another thread: take the default action. | ||
| // `sig` is blocked in its own handler, so the raise lands on return. | ||
|
robobun marked this conversation as resolved.
|
||
| QUEUED.store(false, Ordering::SeqCst); | ||
| // SAFETY: zeroed `sigaction` is valid; both calls are async-signal-safe. | ||
| unsafe { | ||
| let mut act: libc::sigaction = bun_core::ffi::zeroed(); | ||
| act.sa_sigaction = libc::SIG_DFL; | ||
| libc::sigaction(sig, &raw const act, core::ptr::null_mut()); | ||
| libc::raise(sig); | ||
| } | ||
| return; | ||
| }; | ||
| // SAFETY: the loop is live while hooked; `task` is not queued (`QUEUED` | ||
| // was false). Raw form because this may interrupt the loop inside `tick`. | ||
| unsafe { MiniEventLoop::enqueue_task_concurrent_raw(event_loop, task) }; | ||
| } | ||
|
|
||
| fn run_task(_: *mut (), _: *mut ()) { | ||
| QUEUED.store(false, Ordering::SeqCst); | ||
| let on_signal = *ON_SIGNAL.lock(); | ||
| if let (Some(on_signal), Some(sig)) = (on_signal, received()) { | ||
| on_signal(sig); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.