1use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use crate::{EngineError, Pid, RuntimeHandle};
7
8use super::outcome::{self, WorkflowProcessOutcome};
9
10#[derive(Clone)]
12pub struct ProcessMonitorHandle {
13 installed: Arc<AtomicBool>,
14}
15
16impl ProcessMonitorHandle {
17 fn installed() -> Self {
18 Self {
19 installed: Arc::new(AtomicBool::new(true)),
20 }
21 }
22
23 #[must_use]
25 pub fn is_installed(&self) -> bool {
26 self.installed.load(Ordering::Acquire)
27 }
28}
29
30impl RuntimeHandle {
31 pub fn monitor_process<F>(
46 self: &Arc<Self>,
47 pid: Pid,
48 callback: F,
49 ) -> Result<ProcessMonitorHandle, EngineError>
50 where
51 F: FnOnce(Result<WorkflowProcessOutcome, EngineError>) + Send + 'static,
52 {
53 self.ensure_monitorable_pid(pid)?;
54 let runtime = Arc::clone(self);
55 std::thread::Builder::new()
56 .name(format!("aion-workflow-monitor-{pid}"))
57 .spawn(move || {
58 let outcome =
59 outcome::workflow_process_outcome(&runtime.scheduler, &runtime.atom_table, pid);
60 runtime.release_spawn_heaps(pid);
61 runtime.nif_state().cleanup_process(pid);
62 runtime.drain_activity_completions(pid);
66 callback(outcome);
67 })
68 .map_err(|error| EngineError::Runtime {
69 reason: format!("failed to spawn workflow monitor for process {pid}: {error}"),
70 })?;
71 Ok(ProcessMonitorHandle::installed())
72 }
73
74 #[cfg(test)]
80 pub fn monitor_process_for_test<F>(
81 self: &Arc<Self>,
82 pid: Pid,
83 callback: F,
84 ) -> Result<ProcessMonitorHandle, EngineError>
85 where
86 F: FnOnce(Result<WorkflowProcessOutcome, EngineError>) + Send + 'static,
87 {
88 self.monitor_process(pid, callback)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use std::sync::Arc;
95 use std::sync::mpsc;
96 use std::time::Duration;
97
98 use crate::runtime::{RuntimeConfig, RuntimeHandle};
99
100 type TestResult = Result<(), Box<dyn std::error::Error>>;
101
102 #[test]
103 fn monitor_installs_for_process_that_already_exited() -> TestResult {
104 let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
105 let pid = runtime.spawn_test_process()?;
106 runtime.cancel_pid(pid)?;
107 assert!(
108 !runtime.is_live(pid),
109 "terminated test process should leave the live table"
110 );
111
112 let (sender, receiver) = mpsc::channel();
116 let handle = runtime.monitor_process_for_test(pid, move |outcome| {
117 let _ = sender.send(outcome.is_ok());
118 })?;
119
120 assert!(handle.is_installed());
121 let callback_fired = receiver.recv_timeout(Duration::from_secs(10))?;
122 let _ = callback_fired;
125 runtime.shutdown()?;
126 Ok(())
127 }
128
129 #[test]
130 fn monitor_rejects_pid_never_spawned_by_this_runtime() -> TestResult {
131 let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
132
133 let error = runtime
134 .monitor_process_for_test(9_999, |_| {})
135 .err()
136 .ok_or("monitor accepted a pid this runtime never spawned")?;
137
138 assert!(error.to_string().contains("never spawned"));
139 runtime.shutdown()?;
140 Ok(())
141 }
142}