diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bdb5e5..10adcc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable milestones for the nera project are documented in this file. +## Fatal self-exit signals stop execution immediately + +- `exit(self(), Reason)` now stops before the next expression when the signal + is fatal: `kill` even while trapping exits, or any reason while not trapping. + Catch handlers and `after` bodies do not run for an exit signal, as on BEAM. +- A self-sent `normal` signal terminates a process that is not trapping exits; + `normal` from another process is still ignored. Trapped non-`kill` signals + remain messages, and `kill` still becomes the monitor reason `killed`. +- Pending signals retain their provenance separately from message contents. + Ordinary or already-trapped `{'EXIT', Pid, Reason}` messages cannot become + fatal signals when a later exit signal arrives. +- The `exit_self` regression runs unchanged natively and on BEAM, covering + direct and dynamic calls, tail calls, handlers and receive resumption. + ## Stored bitstrings keep their bytes and exact bit length - ETS and `persistent_term` now own copies of non-byte-aligned bitstrings, diff --git a/crates/nera-runtime/src/ffi/process.rs b/crates/nera-runtime/src/ffi/process.rs index f7ce5fc..3b3d40d 100644 --- a/crates/nera-runtime/src/ffi/process.rs +++ b/crates/nera-runtime/src/ffi/process.rs @@ -1874,7 +1874,7 @@ pub unsafe extern "C" fn nera_rt_is_process_alive(ctx: *mut ProcessContext, term }) } -/// erlang:exit/2 — send an exit signal to another process. +/// erlang:exit/2 — send an exit signal, yielding immediately for a fatal self-signal. #[no_mangle] pub unsafe extern "C" fn nera_rt_exit_2(ctx: *mut ProcessContext, pid_term: u64, reason: u64) -> u64 { let target_pid = match ErlTerm(pid_term).as_pid_id() { @@ -1894,6 +1894,15 @@ pub unsafe extern "C" fn nera_rt_exit_2(ctx: *mut ProcessContext, pid_term: u64, }); let fragment = crate::message::MessageFragment::from_term(exit_msg); entry.inbox.push(crate::message::MessageFragment::ExitSignal(Box::new(fragment))); + if target_pid == self_pid && !ctx.is_null() { + let trap_exit = unsafe { (*(*ctx).otp_ptr).trap_exit }; + if !trap_exit || ErlTerm(reason).as_atom_id() == Some(crate::atom_table::ATOM_KILL) { + // Return to the scheduler before the next expression. + // It consumes the queued signal before resuming any + // caller, so neither catch nor after can intercept it. + unsafe { (*ctx).status = crate::process::STATUS_YIELD; } + } + } // Try waking the process. try_wake() handles receive-waiting // (ASTATUS_WAITING), try_wake_io() handles IO-waiting // (ASTATUS_IO_WAIT). Only one CAS can succeed. diff --git a/crates/nera-runtime/src/message.rs b/crates/nera-runtime/src/message.rs index 9ed87d7..133b1ee 100644 --- a/crates/nera-runtime/src/message.rs +++ b/crates/nera-runtime/src/message.rs @@ -161,9 +161,9 @@ pub struct HeapFragment { /// inbox (`SegQueue`). The receiving worker drains fragments into the process /// mailbox under its own Mutex. pub enum MessageFragment { - /// EXIT classification travels with the payload in the same queue push. - /// The worker marks the mailbox for signal handling when it drains this, - /// before a self-wake can treat the payload as ordinary mail. + /// Pending exit/2 signal, distinct from an ordinary {'EXIT', Pid, Reason} + /// message. Classification travels with the payload in one queue push; + /// inbox draining retains it until signal processing. ExitSignal(Box), /// Immediate value (atom, small int, pid, nil) — no heap needed. Immediate(ImmediateTerm), @@ -313,7 +313,6 @@ impl MessageFragment { /// Consumes the fragment. pub fn copy_to_heap(self, target: &mut ProcessHeap) -> ErlTerm { match self { - MessageFragment::ExitSignal(signal) => signal.copy_to_heap(target), MessageFragment::Immediate(term) => term.get(), MessageFragment::RefcBinary(range) => { // Move the validated window and its Arc without another check or clone. @@ -335,8 +334,8 @@ impl MessageFragment { target.adopt_fragment(frag.heap); frag.root } - MessageFragment::ActiveReadReady { .. } => { - panic!("ActiveReadReady must be intercepted by drain_inbox, not copy_to_heap"); + MessageFragment::ActiveReadReady { .. } | MessageFragment::ExitSignal(_) => { + panic!("runtime notifications must be intercepted by drain_inbox, not copy_to_heap"); } } } diff --git a/crates/nera-runtime/src/process.rs b/crates/nera-runtime/src/process.rs index 53f06e7..063c9bc 100644 --- a/crates/nera-runtime/src/process.rs +++ b/crates/nera-runtime/src/process.rs @@ -579,11 +579,14 @@ fn drain_inbox_into(entry: &ProcessEntry, proc: &mut Process) { handle_active_read_on_worker(proc, stream_handle, &in_flight); } } + MessageFragment::ExitSignal(signal) => { + let msg = signal.copy_to_heap(&mut proc.heap); + proc.mailbox.send_exit_signal(msg); + entry.has_exit_signals.store(true, Ordering::Release); + } other => { - let is_exit = matches!(&other, MessageFragment::ExitSignal(_)); let msg = other.copy_to_heap(&mut proc.heap); proc.mailbox.send(msg); - if is_exit { entry.has_exit_signals.store(true, Ordering::Relaxed); } } } return; @@ -611,11 +614,14 @@ fn drain_inbox_into(entry: &ProcessEntry, proc: &mut Process) { handle_active_read_on_worker(proc, stream_handle, &in_flight); } } + MessageFragment::ExitSignal(signal) => { + let msg = signal.copy_to_heap(&mut proc.heap); + proc.mailbox.send_exit_signal(msg); + entry.has_exit_signals.store(true, Ordering::Release); + } other => { - let is_exit = matches!(&other, MessageFragment::ExitSignal(_)); let msg = other.copy_to_heap(&mut proc.heap); proc.mailbox.send(msg); - if is_exit { entry.has_exit_signals.store(true, Ordering::Relaxed); } } } } @@ -2137,13 +2143,13 @@ impl Process { // Exit signal processing // --------------------------------------------------------------------------- -/// Scan the process mailbox for `{'EXIT', FromPid, Reason}` tuples and -/// process them according to BEAM semantics: +/// Process pending exit/2 signals in mailbox order, never ordinary or already +/// trapped `{'EXIT', FromPid, Reason}` messages, according to BEAM semantics: /// /// - `kill` reason: always kill (untrappable) — returns `Some(ATOM_KILLED)` /// - `trap_exit=true` + non-kill: leave in mailbox as regular message -/// - `trap_exit=false` + `normal`: silently remove (BEAM ignores normal exits) -/// - `trap_exit=false` + non-normal, non-kill: kill — returns `Some(reason)` +/// - `trap_exit=false` + `normal` from another process: silently remove +/// - `trap_exit=false` + self-sent `normal` or any other reason: kill /// /// Returns `Some(exit_reason)` if the process should die. /// @@ -2178,6 +2184,11 @@ pub(crate) unsafe fn process_exit_signals(proc: &mut Process) -> Option let buffer = proc.mailbox.buf; for offset in 0..slots { let physical = (head.wrapping_add(offset) & mask) as usize; + // Provenance belongs to the mailbox slot, not the tuple's contents. + // Once a signal is trapped it remains an ordinary message on later scans. + if !proc.mailbox.exit_signals.remove(&physical) { + continue; + } // SAFETY: each original occupied slot was initialized. This loop only // tombstones/removes slots: no resize or append invalidates the buffer. let msg = ErlTerm(unsafe { *buffer.add(physical) }); @@ -2223,8 +2234,11 @@ pub(crate) unsafe fn process_exit_signals(proc: &mut Process) -> Option continue; } - // trap_exit=false - if reason_atom == Some(ATOM_NORMAL) { + // A normal signal from another process is ignored, but an explicit + // exit(self(), normal) terminates a process that is not trapping exits. + // SAFETY: the same live three-element tuple contains its sender PID. + let from_pid = unsafe { ErlTerm(*ptr.add(2)) }; + if reason_atom == Some(ATOM_NORMAL) && from_pid != ErlTerm::pid(proc.id) { // SAFETY: this live slot was just read from the stable scan buffer; // no intervening operation changes storage or removes this slot. unsafe { @@ -2233,7 +2247,7 @@ pub(crate) unsafe fn process_exit_signals(proc: &mut Process) -> Option continue; } - // Non-normal, non-kill: kill the process with this reason + // Fatal signal, including self-sent normal: exit with this reason // SAFETY: this live slot belongs to the unchanged scan buffer. unsafe { proc.mailbox.remove_scanned_slot(physical); @@ -2269,6 +2283,9 @@ pub struct Mailbox { scan_pos: u32, /// Number of tombstoned (removed) slots within `[head, head+len)`. tombstone_count: u32, + /// Physical slots containing pending exit/2 signals, not ordinary messages. + /// Empty before compiled receive code runs; GC preserves slot positions. + exit_signals: HashSet, } // Field offsets for codegen inlining. @@ -2313,6 +2330,7 @@ impl Mailbox { len: 0, scan_pos: 0, tombstone_count: 0, + exit_signals: HashSet::new(), } } @@ -2356,6 +2374,7 @@ impl Mailbox { assert!(!new_buf.is_null(), "allocation failed"); let mut write = 0u32; + let mut exit_signals = HashSet::new(); if self.len > 0 && !self.buf.is_null() { for i in 0..self.len { let src_idx = self.physical_index(i); @@ -2366,6 +2385,9 @@ impl Mailbox { unsafe { *new_buf.add(write as usize) = val; } + if self.exit_signals.contains(&src_idx) { + exit_signals.insert(write as usize); + } write += 1; } } @@ -2381,6 +2403,7 @@ impl Mailbox { } self.buf = new_buf; + self.exit_signals = exit_signals; self.capacity = new_cap; self.head = 0; self.len = kept; @@ -2422,6 +2445,12 @@ impl Mailbox { self.len += 1; } + /// Append an exit/2 signal with provenance that ordinary sends cannot forge. + fn send_exit_signal(&mut self, msg: ErlTerm) { + self.send(msg); + self.exit_signals.insert(self.physical_index(self.len - 1)); + } + /// Number of live messages currently in the mailbox. #[inline] pub fn len(&self) -> usize { @@ -2472,6 +2501,7 @@ impl Mailbox { let msg = ErlTerm(unsafe { *self.buf.add(phys) }); assert!(!msg.is_tombstone(), "remove_at: double-remove of tombstone"); + self.exit_signals.remove(&phys); // Write tombstone sentinel. unsafe { *self.buf.add(phys) = crate::term::TOMBSTONE_VALUE; @@ -2493,6 +2523,7 @@ impl Mailbox { debug_assert!(physical < self.capacity as usize); // SAFETY: caller establishes initialized, live storage within capacity. debug_assert!(!ErlTerm(unsafe { *self.buf.add(physical) }).is_tombstone()); + self.exit_signals.remove(&physical); // SAFETY: caller establishes the writable slot; &mut self is exclusive. unsafe { *self.buf.add(physical) = crate::term::TOMBSTONE_VALUE; } self.tombstone_count += 1; @@ -2535,12 +2566,16 @@ impl Mailbox { debug_assert_eq!(self.scan_pos, 0, "compact: must not run during active scan"); let alive = self.len - self.tombstone_count; let mut write_pos: u32 = 0; + let mut exit_signals = HashSet::new(); for read_pos in 0..self.len { let phys_r = self.physical_index(read_pos); let val = unsafe { *self.buf.add(phys_r) }; if val != crate::term::TOMBSTONE_VALUE { + let phys_w = self.physical_index(write_pos); + if self.exit_signals.contains(&phys_r) { + exit_signals.insert(phys_w); + } if write_pos != read_pos { - let phys_w = self.physical_index(write_pos); unsafe { *self.buf.add(phys_w) = val; } @@ -2549,6 +2584,7 @@ impl Mailbox { } } debug_assert_eq!(write_pos, alive); + self.exit_signals = exit_signals; self.len = alive; self.tombstone_count = 0; } @@ -2879,7 +2915,7 @@ mod tests { ErlTerm::pid(crate::process::ProcessId::new(2)), ErlTerm::atom(reason), ]); - proc.mailbox.send(signal); + proc.mailbox.send_exit_signal(signal); } // Fill the ring so the pre-fix stale-length read cannot read uninitialized // slots; it still incorrectly skips the kill after removing the head. @@ -2908,7 +2944,7 @@ mod tests { let signal = proc.heap .alloc_tuple(&[ErlTerm::atom(ATOM_EXIT_SIGNAL), ErlTerm::pid(crate::process::ProcessId::new(2)), reason]); - proc.mailbox.send(signal); + proc.mailbox.send_exit_signal(signal); // SAFETY: all roots are immediate or initialized on this process heap, // held exclusively; the mailbox roots are updated by the collector. unsafe { proc.collect_garbage_full() }; @@ -2982,11 +3018,11 @@ mod tests { for _ in 0..14 { proc.mailbox.remove_at(0); } - proc.mailbox.send(normal); + proc.mailbox.send_exit_signal(normal); proc.mailbox.send(ErlTerm::small_int(99)); - proc.mailbox.send(normal); - proc.mailbox.send(normal); - proc.mailbox.send(kill); + proc.mailbox.send_exit_signal(normal); + proc.mailbox.send_exit_signal(normal); + proc.mailbox.send_exit_signal(kill); proc.mailbox.remove_at(3); // a tombstone between EXIT signals proc.mailbox.remove_at(0); proc.mailbox.remove_at(0); // head wraps during the next signal removal @@ -3005,12 +3041,72 @@ mod tests { ErlTerm::atom(ATOM_NORMAL), ]); for _ in 0..3 { - proc.mailbox.send(normal); + proc.mailbox.send_exit_signal(normal); } // SAFETY: every queued tuple is initialized on this live, unmoving heap. assert_eq!(unsafe { process_exit_signals(&mut proc) }, None); assert!(proc.mailbox.is_empty()); } + #[test] + fn exit_signal_provenance_survives_mailbox_growth_and_compaction() { + use crate::atom_table::{ATOM_EXIT_SIGNAL, ATOM_KILL, ATOM_KILLED, ATOM_NORMAL}; + let mut proc = Process::new(ProcessId::new(1)); + let ordinary = proc.heap.alloc_tuple(&[ + ErlTerm::atom(ATOM_EXIT_SIGNAL), ErlTerm::pid(proc.id), ErlTerm::atom(ATOM_NORMAL), + ]); + let signal = proc.heap.alloc_tuple(&[ + ErlTerm::atom(ATOM_EXIT_SIGNAL), ErlTerm::pid(proc.id), ErlTerm::atom(ATOM_KILL), + ]); + // Start with a wrapped ring so growing moves the signal's physical slot. + for i in 0..16 { proc.mailbox.send(ErlTerm::small_int(i)); } + for _ in 0..14 { proc.mailbox.remove_at(0); } + proc.mailbox.send(ordinary); + for i in 0..10 { proc.mailbox.send(ErlTerm::small_int(i)); } + proc.mailbox.send_exit_signal(signal); + for i in 0..64 { proc.mailbox.send(ErlTerm::small_int(i)); } + // Remove the leading padding and leave tombstones before and after the + // signal. Compaction must move its provenance with its message. + proc.mailbox.remove_at(0); + proc.mailbox.remove_at(0); + for i in 1..11 { proc.mailbox.remove_at(i); } + for i in 12..60 { proc.mailbox.remove_at(i); } + proc.mailbox.reset_scan(); + // SAFETY: all terms are owned by this live, exclusively accessed heap. + assert_eq!(unsafe { process_exit_signals(&mut proc) }, Some(ErlTerm::atom(ATOM_KILLED))); + assert_eq!(proc.mailbox.messages().next(), Some(ordinary)); + } + + #[test] + fn exit_message_provenance_survives_single_and_batch_inbox_drain() { + use crate::atom_table::{ATOM_EXIT_SIGNAL, ATOM_KILL, ATOM_KILLED, ATOM_NORMAL, ATOM_TRUE}; + for single_first in [false, true] { + let entry = ProcessEntry::new(Process::new(ProcessId::new(1))); + let mut proc = entry.lock_for_runtime(); + let self_pid = ErlTerm::pid(proc.id); + let ordinary = proc.heap.alloc_tuple(&[ + ErlTerm::atom(ATOM_EXIT_SIGNAL), self_pid, ErlTerm::atom(ATOM_NORMAL), + ]); + let signal = proc.heap.alloc_tuple(&[ + ErlTerm::atom(ATOM_EXIT_SIGNAL), self_pid, ErlTerm::atom(ATOM_KILL), + ]); + // SAFETY: both source graphs remain live and unmoving on this heap. + entry.inbox.push(unsafe { MessageFragment::from_term(ordinary) }); + if single_first { proc.drain_inbox(); } + entry.inbox.push(MessageFragment::ExitSignal(Box::new( + unsafe { MessageFragment::from_term(signal) } + ))); + proc.drain_inbox(); + // SAFETY: draining transferred both graphs into this locked process. + assert_eq!(unsafe { process_exit_signals(&mut proc) }, Some(ErlTerm::atom(ATOM_KILLED))); + let kept = proc.mailbox.messages().next().unwrap(); + // SAFETY: the original and delivered tuple are still owned and live. + assert_eq!(unsafe { + crate::ffi::compare::nera_rt_exact_eq(std::ptr::null_mut(), kept.0, ordinary.0) + }, ErlTerm::atom(ATOM_TRUE).0); + assert_eq!(proc.mailbox.len(), 1); + } + } + #[test] fn mailbox_rejects_overflowing_growth_without_mutation() { for active_scan in [false, true] { diff --git a/crates/nera-runtime/src/scheduler.rs b/crates/nera-runtime/src/scheduler.rs index 74a612c..27bf382 100644 --- a/crates/nera-runtime/src/scheduler.rs +++ b/crates/nera-runtime/src/scheduler.rs @@ -724,7 +724,9 @@ fn propagate_exit_signals( }; drop(proc); - entry.inbox.push(MessageFragment::ExitSignal(Box::new(fragment))); + // Trapping has already converted this link signal into a message. + // Do not reinterpret it if trap_exit changes before delivery. + entry.inbox.push(fragment); // Try waking the process. try_wake() handles receive-waiting, // try_wake_io() handles IO-waiting. Only one CAS can succeed. // The worker that runs the process cancels its receive timer; diff --git a/crates/nera-runtime/tests/exit_signal_contract.rs b/crates/nera-runtime/tests/exit_signal_contract.rs index 685f27c..e9d91fc 100644 --- a/crates/nera-runtime/tests/exit_signal_contract.rs +++ b/crates/nera-runtime/tests/exit_signal_contract.rs @@ -1,5 +1,5 @@ -//! Check the real private EXIT adapter requires an unsafe call inside the crate, -//! independently of its visibility to external library users. +//! Check the real private EXIT adapter's unsafe call boundary independently of +//! its external visibility. Runtime tests own traversal and signal semantics. use std::process::Command; #[test] @@ -7,49 +7,45 @@ fn raw_exit_scan_requires_an_unsafe_call() { let source = include_str!("../src/process.rs"); let signature = source.find("fn process_exit_signals").unwrap(); let start = source[..signature].rfind('\n').unwrap() + 1; - let end = source[start..] - .find("\n// ---------------------------------------------------------------------------") + let end = source[signature..].find('{').unwrap() + signature; + let declaration = &source[start..end]; + // Only the declaration controls whether a call requires unsafe. Compile it + // with the real runtime types, without duplicating private mailbox state or + // substituting fake term/heap operations for the implementation. + let adapter = format!("{declaration}{{ unreachable!() }}"); + let dependencies = std::env::current_exe() .unwrap() - + start; - let adapter = &source[start..end]; + .parent() + .unwrap() + .to_path_buf(); + let library = std::fs::read_dir(&dependencies) + .unwrap() + .map(|entry| entry.unwrap().path()) + .find(|path| { + path.extension() + .is_some_and(|extension| extension == "rlib") + && path + .file_name() + .unwrap() + .to_string_lossy() + .starts_with("libnera_runtime-") + }) + .expect("the runtime library built for this integration test"); let directory = std::env::temp_dir().join(format!("nera-exit-contract-{}", std::process::id())); std::fs::create_dir_all(&directory).unwrap(); let file = directory.join("contract.rs"); - let prelude = r#" -mod atom_table { - pub const ATOM_EXIT_SIGNAL: u32 = 1; - pub const ATOM_KILL: u32 = 2; - pub const ATOM_KILLED: u32 = 3; - pub const ATOM_NORMAL: u32 = 4; -} -mod term { - pub const BOXED_TUPLE: u64 = 1; - pub const TOMBSTONE_VALUE: u64 = 7; - pub struct BoxedHeader(pub u64); - impl BoxedHeader { - pub fn subtype(&self) -> u64 { 0 } - pub fn arity(&self) -> usize { 0 } - } -} -pub struct ErlTerm(pub u64); -impl ErlTerm { - fn is_boxed(&self) -> bool { false } - fn as_boxed_ptr(&self) -> Option<*const u64> { None } - fn as_atom_id(&self) -> Option { None } - fn atom(id: u32) -> Self { Self(id as u64) } -} -struct Mailbox { head: u32, capacity: u32, buf: *mut u64 } -impl Mailbox { - fn slot_count(&self) -> u32 { 0 } - unsafe fn remove_scanned_slot(&mut self, _: usize) {} -} -struct Otp { trap_exit: bool } -pub struct Process { mailbox: Mailbox, otp: Otp } -"#; let compile = |body: &str| { - std::fs::write(&file, format!("{prelude}\n{adapter}\n{body}")).unwrap(); + std::fs::write( + &file, + format!("use nera_runtime::{{process::Process, term::ErlTerm}};\n{adapter}\n{body}"), + ) + .unwrap(); Command::new("rustc") .args(["--edition=2021", "--crate-type=lib", "--emit=metadata"]) + .arg("--extern") + .arg(format!("nera_runtime={}", library.display())) + .arg("-L") + .arg(format!("dependency={}", dependencies.display())) .arg(&file) .arg("-o") .arg(directory.join("contract.rmeta")) diff --git a/tests/README.md b/tests/README.md index 14b8876..46caaf4 100644 --- a/tests/README.md +++ b/tests/README.md @@ -385,6 +385,7 @@ These are automatically skipped by the runner and show up as SKIP in output. | `process_dict_exact_keys.erl` | put/get/erase exact numeric and compound keys, signed zeros, equal reconstructed keys, missing lookup/removal, replacement/erase returns, dictionary contents and direct/qualified/dynamic/fun calls; also run on BEAM | Table-driven | | `process_dict_get_keys.erl` | get_keys/1 exact value filtering, auto-import/qualified/dynamic/fun calls, dictionary preservation, arbitrary keys, updates/erase and Ranch child-state lookup; also run on BEAM | Table-driven | | `process_flag_basic.erl` | process_flag(trap_exit, true) | ~4 | +| `exit_self.erl` | Fatal `exit(self(), Reason)` stops before later effects, catch handlers or after bodies: kill with either trap setting, untrapped normal and other term reasons, direct/apply/computed/fun/tail calls and receive resumption; trapped non-kill messages, external normal and ordinary/already-trapped EXIT tuple provenance controls; same fixture on BEAM | Table-driven | | `process_info.erl` | process_info/1,2 | ~6 | | `reference.erl` | make_ref, is_reference | ~6 | | `scheduler_yield.erl` | Preemptive yield in tight loops | ~4 | diff --git a/tests/exit_self.erl b/tests/exit_self.erl new file mode 100644 index 0000000..aa27a79 --- /dev/null +++ b/tests/exit_self.erl @@ -0,0 +1,195 @@ +%% EXPECT: exit_self_ok +%% A fatal exit(self(), Reason) stops before the next expression, without +%% entering catch handlers or after bodies. Run unchanged natively and on BEAM. +-module(exit_self). +-export([test/0]). + +test() -> + Reasons = [kill, normal, shutdown, killed, {shutdown, [worker, 42]}, + 17, <<"stopped">>], + Calls = [direct, qualified, apply_list, apply_fun, computed, fun_ref, + tail_helper, caught, guarded, resumed], + lists:foreach(fun(Reason) -> + lists:foreach(fun(Call) -> + ok = fatal(false, Reason, Call) + end, Calls) + end, Reasons), + %% kill is untrappable, unlike an exit/1 exception with reason kill. + lists:foreach(fun(Call) -> ok = fatal(true, kill, Call) end, Calls), + lists:foreach(fun trapped/1, [normal, shutdown, killed, + {shutdown, [worker, 42]}, 17, <<"stopped">>]), + ok = external_normal(), + lists:foreach(fun(Source) -> + ok = mailbox_control(Source, kill), + ok = mailbox_control(Source, external_normal) + end, [literal, variable, trapped]), + io:format("exit_self_ok~n"), + ok. + +fatal(Trap, Reason, Call) -> + Parent = self(), + {Pid, Ref} = spawn_monitor(fun() -> + process_flag(trap_exit, Trap), + signal(Call, Reason, Parent), + effect(Parent, next_expression), + error(later_error) + end), + Expected = case Reason of kill -> killed; _ -> Reason end, + await_exit(Pid, Ref, Expected). + +signal(direct, Reason, Parent) -> + exit(self(), Reason), + effect(Parent, direct_next); +signal(qualified, Reason, _Parent) -> + erlang:exit(self(), Reason); +signal(apply_list, Reason, _Parent) -> + apply(erlang, exit, id([self(), Reason])); +signal(apply_fun, Reason, _Parent) -> + apply(fun erlang:exit/2, id([self(), Reason])); +signal(computed, Reason, _Parent) -> + Module = id(erlang), + Function = id(exit), + Module:Function(self(), Reason); +signal(fun_ref, Reason, _Parent) -> + Fun = id(fun exit/2), + Fun(self(), Reason); +signal(tail_helper, Reason, Parent) -> + tail_exit(Reason), + effect(Parent, caller); +signal(caught, Reason, Parent) -> + _ = (catch exit(self(), Reason)), + effect(Parent, catch_return); +signal(guarded, Reason, Parent) -> + try + exit(self(), Reason), + effect(Parent, try_body) + catch + _:_ -> effect(Parent, catch_handler) + after + effect(Parent, after_body) + end; +signal(resumed, Reason, Parent) -> + try resumed_exit(Reason, Parent) + catch _:_ -> effect(Parent, resumed_handler) + after effect(Parent, resumed_after) + end. + +tail_exit(Reason) -> + exit(self(), Reason). + +resumed_exit(Reason, Parent) -> + Parent ! {ready, self()}, + receive go -> exit(self(), Reason) end. + +effect(Parent, Where) -> + Parent ! {unexpected, self(), Where}. + +await_exit(Pid, Ref, Expected) -> + receive + {ready, Pid} -> + Pid ! go, + await_exit(Pid, Ref, Expected); + {unexpected, Pid, Where} -> + error({continued_after_self_exit, Where}); + {'DOWN', Ref, process, Pid, Actual} -> + Expected = Actual, + receive + {unexpected, Pid, Where} -> + error({continued_after_self_exit, Where}) + after 0 -> ok + end + after 5000 -> error({self_exit_timeout, Expected}) + end. + +%% Non-kill signals remain trappable messages, including normal and killed. +trapped(Reason) -> + Parent = self(), + {Pid, Ref} = spawn_monitor(fun() -> + false = process_flag(trap_exit, true), + Self = self(), + Self ! before_signal, + true = exit(Self, Reason), + receive before_signal -> ok after 5000 -> error(missing_before) end, + receive + {'EXIT', Self, Reason} -> ok + after 5000 -> error(missing_trapped_exit) + end, + true = process_flag(trap_exit, false), + Parent ! {trapped, Self} + end), + receive + {trapped, Pid} -> ok; + {'DOWN', Ref, process, Pid, Why} -> error({trapped_self_exit, Why}) + after 5000 -> error(trapped_timeout) + end, + await_exit(Pid, Ref, normal). + +%% Unlike self-sent normal, normal from another process is ignored. +external_normal() -> + Parent = self(), + {Pid, Ref} = spawn_monitor(fun() -> + Parent ! {ready, self()}, + receive continue -> Parent ! {survived_normal, self()} end + end), + receive {ready, Pid} -> ok after 5000 -> error(ready_timeout) end, + true = exit(Pid, normal), + Pid ! continue, + receive + {survived_normal, Pid} -> ok; + {'DOWN', Ref, process, Pid, Why} -> error({external_normal_exit, Why}) + after 5000 -> error(external_normal_timeout) + end, + await_exit(Pid, Ref, normal). + +%% An ordinary or previously trapped EXIT tuple is never a pending signal. +%% Check both fused tuple sends and sends of an existing tuple. The trapped +%% case leaves the delivered message queued while a selective receive waits. +mailbox_control(Source, Next) -> + Parent = self(), + {Pid, Ref} = spawn_monitor(fun() -> + Self = self(), + case Source of + literal -> Self ! {'EXIT', Self, normal}; + variable -> + Message = id({'EXIT', Self, normal}), + Self ! Message; + trapped -> + false = process_flag(trap_exit, true), + true = exit(Self, normal), + Parent ! {ready, Self}, + receive go -> ok after 5000 -> error(trap_barrier_timeout) end, + true = process_flag(trap_exit, false) + end, + case Next of + kill -> + exit(Self, kill), + effect(Parent, mailbox_after_kill); + external_normal -> + Parent ! {normal_ready, Self}, + receive go -> ok after 5000 -> error(normal_barrier_timeout) end, + receive + {'EXIT', Self, normal} -> Parent ! {checked, Self} + after 5000 -> error(missing_ordinary_exit_message) + end + end + end), + case Next of + kill -> await_exit(Pid, Ref, killed); + external_normal -> await_checked(Pid, Ref) + end. + +await_checked(Pid, Ref) -> + receive + {ready, Pid} -> + Pid ! go, + await_checked(Pid, Ref); + {normal_ready, Pid} -> + true = exit(Pid, normal), + Pid ! go, + await_checked(Pid, Ref); + {checked, Pid} -> await_exit(Pid, Ref, normal); + {'DOWN', Ref, process, Pid, Why} -> error({ordinary_exit_became_signal, Why}) + after 5000 -> error(mailbox_control_timeout) + end. + +id(Value) -> Value.