use core::ptr;
use bun_core::Output;
use bun_spawn::{Process, ProcessExitKind, Rusage, Status};
pub type ProductExitHook =
unsafe fn(ctx: *mut (), process: &mut Process, status: Status, rusage: &Rusage);
pub struct ProductProcessExitState {
pub last_status: Option<Status>,
pub exit_count: u32,
pub process: *mut Process,
pub release_on_exit: bool,
pub kind: ProcessExitKind,
pub hook_ctx: *mut (),
pub hook: Option<ProductExitHook>,
}
impl ProductProcessExitState {
#[inline]
pub fn new(kind: ProcessExitKind) -> Self {
Self {
last_status: None,
exit_count: 0,
process: ptr::null_mut(),
release_on_exit: false,
kind,
hook_ctx: ptr::null_mut(),
hook: None,
}
}
#[inline]
pub fn attach_process(&mut self, process: *mut Process, take_ref: bool) {
self.process = process;
self.release_on_exit = take_ref && !process.is_null();
}
#[inline]
pub fn set_hook(&mut self, ctx: *mut (), hook: Option<ProductExitHook>) {
self.hook_ctx = ctx;
self.hook = hook;
}
pub unsafe fn on_process_exit(
&mut self,
process: &mut Process,
status: Status,
rusage: &Rusage,
) {
process.status = status.clone();
self.last_status = Some(status.clone());
self.exit_count = self.exit_count.saturating_add(1);
let owned = self.process;
let should_release = self.release_on_exit
&& !owned.is_null()
&& core::ptr::eq(owned, process as *mut Process);
self.process = ptr::null_mut();
self.release_on_exit = false;
if should_release {
unsafe { Process::deref(owned) };
}
Output::debug(format_args!(
"<d>[ProductProcessExit:{:?}]<r> exit#{} status={}",
self.kind, self.exit_count, status
));
if let Some(hook) = self.hook {
unsafe { hook(self.hook_ctx, process, status, rusage) };
}
}
}
macro_rules! product_process_exit {
($variant:ident => $ty:ident) => {
pub struct $ty {
pub state: ProductProcessExitState,
}
impl $ty {
#[inline]
pub fn new() -> Self {
Self {
state: ProductProcessExitState::new(ProcessExitKind::$variant),
}
}
#[inline]
pub fn state(&self) -> &ProductProcessExitState {
&self.state
}
#[inline]
pub fn state_mut(&mut self) -> &mut ProductProcessExitState {
&mut self.state
}
#[inline]
pub fn attach_process(&mut self, process: *mut Process, take_ref: bool) {
self.state.attach_process(process, take_ref);
}
#[inline]
pub fn set_hook(&mut self, ctx: *mut (), hook: Option<ProductExitHook>) {
self.state.set_hook(ctx, hook);
}
#[inline]
pub unsafe fn install_on(&mut self, process: &mut Process) {
process.set_exit_handler(unsafe {
bun_spawn::ProcessExit::new(ProcessExitKind::$variant, self as *mut Self)
});
}
}
impl Default for $ty {
#[inline]
fn default() -> Self {
Self::new()
}
}
bun_spawn::link_impl_ProcessExit! {
$variant for $ty => |this| {
on_process_exit(process, status, rusage) =>
(*this).state.on_process_exit(process, status, rusage),
}
}
const _: fn() = || {
let _ = core::mem::size_of::<$ty>();
};
};
}
product_process_exit!(Subprocess => ProductSubprocessExit);
product_process_exit!(Shell => ProductShellExit);
product_process_exit!(CronRegister => ProductCronRegisterExit);
product_process_exit!(CronRemove => ProductCronRemoveExit);
product_process_exit!(ChromeProcess => ProductChromeProcessExit);
product_process_exit!(HostProcess => ProductHostProcessExit);
product_process_exit!(FilterRunHandle => ProductFilterRunHandleExit);
product_process_exit!(MultiRunHandle => ProductMultiRunHandleExit);
product_process_exit!(TestParallelWorker => ProductTestParallelWorkerExit);
#[inline(never)]
pub fn force_link_product_process_exit() {
let _ = force_link_product_process_exit as *const () as usize;
let _ = core::mem::size_of::<ProductSubprocessExit>();
let _ = core::mem::size_of::<ProductShellExit>();
let _ = core::mem::size_of::<ProductFilterRunHandleExit>();
let _ = core::mem::size_of::<ProductMultiRunHandleExit>();
let _ = core::mem::size_of::<ProductTestParallelWorkerExit>();
let _ = core::mem::size_of::<ProductCronRegisterExit>();
let _ = core::mem::size_of::<ProductCronRemoveExit>();
let _ = core::mem::size_of::<ProductChromeProcessExit>();
let _ = core::mem::size_of::<ProductHostProcessExit>();
let _ = ProcessExitKind::Subprocess;
let _ = ProcessExitKind::HostProcess;
}
#[cfg(test)]
mod tests {
use super::*;
use bun_spawn::Exited;
#[test]
fn all_product_variants_construct_with_kinds() {
assert_eq!(
ProductSubprocessExit::new().state.kind,
ProcessExitKind::Subprocess
);
assert_eq!(ProductShellExit::new().state.kind, ProcessExitKind::Shell);
assert_eq!(
ProductCronRegisterExit::new().state.kind,
ProcessExitKind::CronRegister
);
assert_eq!(
ProductCronRemoveExit::new().state.kind,
ProcessExitKind::CronRemove
);
assert_eq!(
ProductChromeProcessExit::new().state.kind,
ProcessExitKind::ChromeProcess
);
assert_eq!(
ProductHostProcessExit::new().state.kind,
ProcessExitKind::HostProcess
);
assert_eq!(
ProductFilterRunHandleExit::new().state.kind,
ProcessExitKind::FilterRunHandle
);
assert_eq!(
ProductMultiRunHandleExit::new().state.kind,
ProcessExitKind::MultiRunHandle
);
assert_eq!(
ProductTestParallelWorkerExit::new().state.kind,
ProcessExitKind::TestParallelWorker
);
}
#[test]
fn attach_process_and_hook_fields() {
let mut owner = ProductSubprocessExit::new();
assert!(owner.state.process.is_null());
assert!(!owner.state.release_on_exit);
assert!(owner.state.hook.is_none());
assert_eq!(owner.state.exit_count, 0);
assert!(owner.state.last_status.is_none());
let sentinel = 0x10 as *mut Process;
owner.attach_process(sentinel, false);
assert_eq!(owner.state.process, sentinel);
assert!(!owner.state.release_on_exit);
owner.attach_process(sentinel, true);
assert!(owner.state.release_on_exit);
static mut HOOK_ARMED: bool = false;
unsafe fn hook(_ctx: *mut (), _process: &mut Process, _status: Status, _rusage: &Rusage) {
unsafe { HOOK_ARMED = true };
}
owner.set_hook(ptr::null_mut(), Some(hook));
assert!(owner.state.hook.is_some());
let _ = unsafe { HOOK_ARMED };
owner.state.last_status = Some(Status::Exited(Exited { code: 3, signal: 0 }));
owner.state.exit_count = 1;
assert!(matches!(
owner.state.last_status,
Some(Status::Exited(Exited { code: 3, signal: 0 }))
));
assert_eq!(owner.state.exit_count, 1);
}
#[test]
fn force_link_symbol_exists() {
force_link_product_process_exit();
}
}