Skip to main content

artisan_middleware/
process_manager.rs

1use dusa_collection_utils::core::errors::{ErrorArrayItem, Errors};
2use dusa_collection_utils::core::logger::LogLevel;
3use dusa_collection_utils::core::types::pathtype::PathType;
4use dusa_collection_utils::core::types::rb::RollingBuffer;
5use dusa_collection_utils::core::types::rwarc::LockWithTimeout;
6use dusa_collection_utils::log;
7use libc::{c_int, kill, SIGKILL, SIGTERM};
8use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
9use nix::unistd::Pid;
10use std::collections::{HashMap, HashSet, VecDeque};
11use std::pin::Pin;
12use std::process::Stdio;
13use std::time::Duration;
14use std::{io, thread};
15
16use procfs::process::{all_processes, Process};
17use tokio::io::{AsyncRead, AsyncReadExt};
18use tokio::process::{Child, Command};
19use tokio::task::JoinHandle;
20
21use crate::aggregator::Metrics;
22use crate::resource_monitor::{MonitorWatchdog, MonitorWatchdogSnapshot, ResourceMonitorLock};
23use crate::state_persistence::{log_error, update_state, AppState};
24
25const RESOURCE_MONITOR_SAMPLE_INTERVAL: Duration = Duration::from_millis(250);
26const STDX_BUFFER_UPDATE_INTERVAL: Duration = Duration::from_millis(500);
27/// A wrapper around [`LockWithTimeout<Child>`] that synchronizes access to a
28/// [`tokio::process::Child`]. This allows safe concurrent reads/writes or attempts to kill
29/// the child within specified timeouts.
30pub struct ChildLock(pub LockWithTimeout<Child>);
31
32/// Holds a [`ChildLock`] plus a resource monitor and an optional handle to a background
33/// monitoring task. This structure is used to manage a spawned child process in an
34/// asynchronous context (Tokio).
35///
36/// - The `monitor_handle` can be used to stop the resource monitor loop if needed.
37/// - The `monitor_std` handle is used to monitor the process standard output/error streams.
38/// - Lock-free watchdog snapshots expose monitor health without requiring access to task handles.
39/// - It's up to the caller to decide if/how to store and use captured output lines.
40/// - The resource monitor tracks CPU/memory usage via `/proc` (Linux-specific).
41pub struct SupervisedChild {
42    /// The locked child process.
43    pub child: ChildLock,
44    /// Resource-monitor lifecycle (sampling task + watchdog), shared with
45    /// [`SupervisedProcess`] via [`ResourceSupervisor`].
46    resources: ResourceSupervisor,
47    /// An optional background task handle for monitoring std_out/err
48    monitor_std: Option<JoinHandle<()>>,
49    /// Internal tracker for standard out
50    stdout_buffer: LockWithTimeout<RollingBuffer>,
51    /// Internal tracker for standard err
52    stderr_buffer: LockWithTimeout<RollingBuffer>,
53    /// Health/heartbeat state for the stdout/stderr monitor loop.
54    stdx_watchdog: MonitorWatchdog,
55}
56
57/// Represents a supervised process that may not have been spawned via [`tokio::process::Command`]
58/// but is still tracked by a PID. Similar to `SupervisedChild`, but manages an existing
59/// process rather than a newly spawned one.
60pub struct SupervisedProcess {
61    /// The process ID (PID) of the target process.
62    pid: Pid,
63    /// Resource-monitor lifecycle (sampling task + watchdog), shared with
64    /// [`SupervisedChild`] via [`ResourceSupervisor`].
65    resources: ResourceSupervisor,
66}
67
68/// The resource-monitor lifecycle -- the `/proc` sampling task plus its health
69/// watchdog -- shared by [`SupervisedChild`] and [`SupervisedProcess`].
70///
71/// Both types used to carry this as three separate fields plus five near-identical
72/// methods; the only difference between the two copies was a word in a log message.
73/// Pulling it out here means there's exactly one place left that starts, stops, and
74/// health-checks a resource-monitor task.
75struct ResourceSupervisor {
76    monitor: ResourceMonitorLock,
77    handle: Option<JoinHandle<()>>,
78    watchdog: MonitorWatchdog,
79}
80
81impl ResourceSupervisor {
82    fn new(pid: i32) -> Result<Self, ErrorArrayItem> {
83        Ok(Self {
84            monitor: ResourceMonitorLock::new(pid)?,
85            handle: None,
86            watchdog: MonitorWatchdog::new(),
87        })
88    }
89
90    /// Starts the sampling loop if it isn't already running, restarting it if the
91    /// previous task died unexpectedly. `pid_hint` is only used for the log message.
92    async fn ensure_running(&mut self, pid_hint: Option<u32>) {
93        if let Some(handle) = &self.handle {
94            if handle.is_finished() {
95                log!(
96                    LogLevel::Warn,
97                    "Resource monitor task finished unexpectedly for pid {:?}, restarting",
98                    pid_hint
99                );
100                self.handle = None;
101            } else {
102                return;
103            }
104        }
105
106        let monitor = self.monitor.clone();
107        let handle: JoinHandle<()> = monitor
108            .monitor_with_watchdog_interval(
109                RESOURCE_MONITOR_SAMPLE_INTERVAL,
110                Some(self.watchdog.clone()),
111            )
112            .await;
113        self.handle = Some(handle);
114    }
115
116    /// Stops the sampling task, if any, via [`JoinHandle::abort()`].
117    fn terminate(&mut self) {
118        if let Some(handle) = &self.handle {
119            log!(LogLevel::Trace, "Terminating monitor");
120            handle.abort();
121            self.handle = None;
122            self.watchdog.mark_stopped();
123        }
124    }
125
126    /// Returns whether the sampling task is currently running, clearing a
127    /// finished-but-not-yet-noticed handle as a side effect.
128    fn is_running(&mut self) -> bool {
129        if let Some(handle) = &self.handle {
130            if handle.is_finished() {
131                self.handle = None;
132                self.watchdog.mark_stopped();
133                false
134            } else {
135                true
136            }
137        } else {
138            false
139        }
140    }
141
142    async fn get_metrics(&self) -> Result<Metrics, ErrorArrayItem> {
143        self.monitor.get_metrics().await
144    }
145
146    fn watchdog_snapshot(&self) -> MonitorWatchdogSnapshot {
147        self.watchdog.snapshot()
148    }
149
150    fn valid(&self, max_staleness: Duration, max_consecutive_failures: u64) -> bool {
151        self.watchdog
152            .snapshot()
153            .is_valid(max_staleness, max_consecutive_failures)
154    }
155
156    /// Stops any running task on `self`, then returns a fresh instance sharing the
157    /// same monitor and watchdog state but with no task of its own.
158    fn clone_idle(&mut self) -> Self {
159        self.terminate();
160        Self {
161            monitor: self.monitor.clone(),
162            handle: None,
163            watchdog: self.watchdog.clone(),
164        }
165    }
166}
167
168impl SupervisedProcess {
169    /// Creates a new `SupervisedProcess` from an existing PID. This checks if the PID is active
170    /// (via `kill(pid, 0)`). If active, it initializes a resource monitor on that PID.
171    ///
172    /// # Errors
173    /// - Returns an [`ErrorArrayItem`] if the PID is not active or if the resource monitor
174    ///   fails to initialize.
175    ///
176    /// # Safety / Platform
177    /// - **Linux-specific**: The `kill` check and `/proc` monitoring assume a Linux-like environment.
178    /// - Using `kill(pid, 0)` is a non-destructive check that returns 0 if the process exists,
179    ///   and `-1` if it doesn’t or if permissions are lacking.
180    pub fn new(pid: Pid) -> Result<Self, ErrorArrayItem> {
181        if !is_pid_active(pid.as_raw()).unwrap_or(false) {
182            return Err(ErrorArrayItem::new(
183                Errors::SupervisedChild,
184                format!(
185                    "Failed to create SupervisedProcess; cannot determine status of PID: {}",
186                    pid
187                ),
188            ));
189        }
190
191        Ok(SupervisedProcess {
192            pid,
193            resources: ResourceSupervisor::new(pid.as_raw())?,
194        })
195    }
196
197    /// Returns the raw PID of this process.
198    pub fn get_pid(&self) -> i32 {
199        self.pid.as_raw()
200    }
201
202    /// Returns the resource monitor backing this process.
203    pub fn monitor(&self) -> &ResourceMonitorLock {
204        &self.resources.monitor
205    }
206
207    /// Terminates the monitored process by:
208    /// 1. Stopping any monitoring task.
209    /// 2. Recursively sending a `SIGTERM` to all processes in the PGID.
210    /// 3. Reaping zombies (via `waitpid`) if the processes exit.
211    /// 4. If any remain after 400ms, sending `SIGKILL`.
212    ///
213    /// # Errors
214    /// - Returns an I/O error if any `kill` syscall fails unexpectedly.
215    /// - Also returns an error if the process cannot be reaped properly.
216    ///
217    /// # Why Reap Zombies?
218    /// - In Linux, a process that has terminated but whose parent hasn't called `wait*()` is
219    ///   marked as a "zombie." Reaping zombies avoids accumulation of defunct processes,
220    ///   freeing kernel resources.
221    pub fn kill(&mut self) -> Result<(), ErrorArrayItem> {
222        self.resources.terminate();
223        let xid = self.pid.as_raw();
224        log!(LogLevel::Trace, "Killing supervised pid {}", xid);
225
226        kill_pgid_recursive(xid)?;
227        Ok(())
228    }
229
230    /// Returns `true` if the process is still active (PID exists), or `false` otherwise.
231    ///
232    /// # Zombie caveat
233    /// Unlike [`SupervisedChild::running`], this can't reap on your behalf: a
234    /// `SupervisedProcess` wraps a bare PID that this instance is frequently *not*
235    /// the real parent of (e.g. re-attached to a PID recorded before a watchdog
236    /// restart). Only the actual parent -- or `init`/a subreaper, once the process
237    /// is reparented -- can reap it. So a zombie still reports as "running" here;
238    /// this only tells you whether the PID still exists in the process table.
239    pub fn running(&self) -> bool {
240        is_pid_active(self.pid.as_raw()).unwrap_or(false)
241    }
242
243    /// Alias for [`SupervisedProcess::running`].
244    pub fn active(&self) -> bool {
245        self.running()
246    }
247
248    /// Clones this `SupervisedProcess`, returning a new instance without a running monitor.
249    /// The existing monitor is terminated before cloning.
250    pub async fn clone(&mut self) -> Self {
251        Self {
252            pid: self.pid,
253            resources: self.resources.clone_idle(),
254        }
255    }
256
257    /// Spawns an asynchronous resource monitoring loop that periodically queries
258    /// `/proc/<pid>` for CPU/memory usage.
259    ///
260    /// # Note
261    /// - Calling this again is a no-op unless the previous monitor task has died.
262    /// - A watchdog is updated on each loop iteration for out-of-band health checks.
263    pub async fn monitor_usage(&mut self) {
264        self.resources
265            .ensure_running(Some(self.pid.as_raw() as u32))
266            .await;
267    }
268
269    /// Terminates the resource monitor task, if any.
270    ///
271    /// # Note
272    /// - Uses [`JoinHandle::abort()`] to stop the task immediately.
273    pub fn terminate_monitor(&mut self) {
274        self.resources.terminate();
275    }
276
277    /// Checks if there is currently a resource monitor running
278    /// for a given [`SupervisedProcess`]
279    pub fn monitoring(&mut self) -> bool {
280        self.resources.is_running()
281    }
282
283    /// Fetches resource usage metrics (CPU, memory, etc.) from the process-specific resource monitor.
284    ///
285    /// # Errors
286    /// - Returns an [`ErrorArrayItem`] if the resource monitor fails to read from `/proc` or
287    ///   if the process does not exist anymore.
288    pub async fn get_metrics(&self) -> Result<Metrics, ErrorArrayItem> {
289        self.resources.get_metrics().await
290    }
291
292    /// Returns a lock-free watchdog snapshot for the resource monitor.
293    pub fn resource_watchdog_snapshot(&self) -> MonitorWatchdogSnapshot {
294        self.resources.watchdog_snapshot()
295    }
296
297    /// Returns whether the resource monitor appears healthy.
298    pub fn resource_monitor_valid(
299        &self,
300        max_staleness: Duration,
301        max_consecutive_failures: u64,
302    ) -> bool {
303        self.resources.valid(max_staleness, max_consecutive_failures)
304    }
305}
306
307impl SupervisedChild {
308    /// Spawns a new child process with its own process group and optionally captures stdout/stderr.
309    /// The resulting process is wrapped in a [`SupervisedChild`] which provides:
310    /// - Locking for the child handle
311    /// - A resource monitor
312    /// - Optional background monitoring
313    /// - Initialized resource/stdx watchdogs
314    ///
315    /// # Behavior
316    /// - Uses [`spawn_complex_process`] under the hood.
317    /// - `true` for capturing output means the child's output is piped rather than inherited.
318    /// - `true` for `independent_process_group` means it calls `setsid()` in `pre_exec` on Linux,
319    ///   so the child won't receive signals from the parent TTY group directly.
320    ///
321    /// # Errors
322    /// - Returns an [`ErrorArrayItem`] if spawning fails or if resource monitoring fails to initialize.
323    pub async fn new(
324        command: &mut Command,
325        working_dir: Option<PathType>,
326    ) -> Result<Self, ErrorArrayItem> {
327        spawn_complex_process(command, working_dir, false, true).await // ! set process group back to false
328    }
329
330    /// Returns the process ID (`PID`) of the child, if available. If locked, tries for a
331    /// read-lock on the child. If no PID is found, an error is returned.
332    ///
333    /// # Errors
334    /// - Returns [`ErrorArrayItem`] if read-lock fails or the PID is invalid.
335    pub async fn get_pid(&self) -> Result<u32, ErrorArrayItem> {
336        let child_lock = &self.child;
337        let child_data = child_lock.0.try_read().await?;
338        match child_data.id() {
339            Some(xid) => Ok(xid),
340            None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid PID").into()),
341        }
342    }
343
344    /// Clones this `SupervisedChild` without active monitor tasks.
345    ///
346    /// This aborts current monitor tasks, then clones the child lock, resource monitor lock,
347    /// buffers, and watchdog state.
348    pub async fn clone(&mut self) -> Self {
349        self.terminate_stdx();
350        let resources = self.resources.clone_idle();
351        let child_lock: ChildLock = self.child.clone();
352
353        Self {
354            child: child_lock,
355            resources,
356            monitor_std: None,
357            stdout_buffer: self.stdout_buffer.clone(),
358            stderr_buffer: self.stderr_buffer.clone(),
359            stdx_watchdog: self.stdx_watchdog.clone(),
360        }
361    }
362
363    /// Recursively terminates the child process group. Sends `SIGTERM` to all
364    /// descendant PIDs and then `SIGKILL` to any that remain.
365    ///
366    /// # Errors
367    /// - Returns an [`ErrorArrayItem`] on I/O issues or if reaping fails.
368    pub async fn kill(&mut self) -> Result<(), ErrorArrayItem> {
369        self.resources.terminate();
370        self.terminate_stdx();
371        self.child.kill().await
372    }
373
374    /// Non-blocking check for whether this child has already exited, reaping it if so.
375    ///
376    /// See [`ChildLock::try_wait`] for details.
377    pub async fn try_wait(&self) -> Result<Option<std::process::ExitStatus>, ErrorArrayItem> {
378        self.child.try_wait().await
379    }
380
381    /// Checks if the child process is still running.
382    ///
383    /// See [`ChildLock::running`] -- this reaps the process via `try_wait` instead of
384    /// signaling the raw PID, so an exited-but-unreaped child (a zombie) is correctly
385    /// reported as not running instead of appearing alive.
386    pub async fn running(&self) -> bool {
387        self.child.running().await
388    }
389
390    /// Returns the resource monitor backing this child.
391    pub fn monitor(&self) -> &ResourceMonitorLock {
392        &self.resources.monitor
393    }
394
395    /// Spawns an asynchronous resource monitoring loop for this child. If a monitor is
396    /// already running, this does nothing.
397    ///
398    /// # Behavior
399    /// - Queries `/proc/<pid>` for CPU, memory, etc. on a sub-second sampling interval.
400    /// - Use [`terminate_monitor`] to stop the task.
401    /// - A watchdog is updated on each loop iteration for out-of-band health checks.
402    pub async fn monitor_usage(&mut self) {
403        let pid_hint = self.get_pid().await.ok();
404        self.resources.ensure_running(pid_hint).await;
405    }
406
407    /// Returns whether the child resource monitor task is currently running.
408    ///
409    /// If the handle exists but has finished, it is cleared and `false` is returned.
410    pub fn monitoring(&mut self) -> bool {
411        self.resources.is_running()
412    }
413
414    /// Spawns an asynchronous resource monitoring loop for the standard out and standard error. If a monitor is
415    /// already running, this does nothing.
416    ///
417    /// # Behavior
418    /// - Acquires the child lock, takes stdout/stderr handles, and streams lines into rolling buffers.
419    /// - Retries on transient lock errors instead of exiting permanently.
420    /// - Use [`terminate_stdx`] to stop the task.
421    pub async fn monitor_stdx(&mut self) {
422        if let Some(handle) = &self.monitor_std {
423            if handle.is_finished() {
424                log!(
425                    LogLevel::Warn,
426                    "Stdout/stderr monitor finished unexpectedly for child pid {:?}, restarting",
427                    self.get_pid().await.ok()
428                );
429                self.monitor_std = None;
430            } else {
431                return;
432            }
433        }
434
435        let child_lock = self.child.clone();
436        let stdout_buffer = self.stdout_buffer.clone();
437        let stderr_buffer = self.stderr_buffer.clone();
438        let stdx_watchdog = self.stdx_watchdog.clone();
439
440        let monitor_handle = tokio::spawn(async move {
441            let mut stdout_task = None;
442            let mut stderr_task = None;
443            stdx_watchdog.mark_started();
444
445            loop {
446                match child_lock.0.try_write().await {
447                    Ok(mut child) => {
448                        if let Some(stdout) = child.stdout.take() {
449                            let reader = Box::pin(stdout) as Pin<Box<dyn AsyncRead + Send>>;
450                            let buffer = stdout_buffer.clone();
451                            stdout_task = Some(tokio::spawn(read_stream_to_buffer(
452                                reader,
453                                buffer,
454                                STDX_BUFFER_UPDATE_INTERVAL,
455                            )));
456                        }
457
458                        if let Some(stderr) = child.stderr.take() {
459                            let reader = Box::pin(stderr) as Pin<Box<dyn AsyncRead + Send>>;
460                            let buffer = stderr_buffer.clone();
461                            stderr_task = Some(tokio::spawn(read_stream_to_buffer(
462                                reader,
463                                buffer,
464                                STDX_BUFFER_UPDATE_INTERVAL,
465                            )));
466                        }
467                        stdx_watchdog.record_success();
468                        break;
469                    }
470                    Err(err) => {
471                        log!(
472                            LogLevel::Warn,
473                            "Failed locking child for stdio monitor: {}",
474                            err
475                        );
476                        stdx_watchdog.record_failure();
477                        tokio::time::sleep(Duration::from_millis(250)).await;
478                    }
479                }
480            }
481
482            if let Some(task) = stdout_task {
483                let _ = task.await;
484            }
485            if let Some(task) = stderr_task {
486                let _ = task.await;
487            }
488            stdx_watchdog.mark_stopped();
489        });
490
491        self.monitor_std = Some(monitor_handle)
492    }
493
494    /// Returns whether the child stdout/stderr monitor task is currently running.
495    ///
496    /// If the handle exists but has finished, it is cleared and `false` is returned.
497    pub fn monitoring_stdx(&mut self) -> bool {
498        if let Some(handle) = &self.monitor_std {
499            if handle.is_finished() {
500                self.monitor_std = None;
501                self.stdx_watchdog.mark_stopped();
502                false
503            } else {
504                true
505            }
506        } else {
507            false
508        }
509    }
510
511    /// Gets the current value of the standard output [`RollingBuffer`] as `Vec<(timestamp, line)>`.
512    pub async fn get_std_out(&self) -> Result<Vec<(u64, String)>, ErrorArrayItem> {
513        let rb = self.stdout_buffer.try_read().await?;
514        Ok(rb.get_latest_time())
515    }
516
517    /// Gets the current value of the standard error [`RollingBuffer`] as `Vec<(timestamp, line)>`.
518    pub async fn get_std_err(&self) -> Result<Vec<(u64, String)>, ErrorArrayItem> {
519        let rb = self.stderr_buffer.try_read().await?;
520        Ok(rb.get_latest_time())
521    }
522
523    /// Terminates the resource monitor task, if any is currently running. This calls
524    /// [`JoinHandle::abort()`] on the stored handle.
525    pub fn terminate_monitor(&mut self) {
526        self.resources.terminate();
527    }
528
529    /// Terminates the stdout/stderr monitor task, if currently running. This calls
530    /// [`JoinHandle::abort()`] on the stored handle.
531    pub fn terminate_stdx(&mut self) {
532        if let Some(handle) = &self.monitor_std {
533            log!(LogLevel::Trace, "Terminating Standart X monitor");
534            handle.abort();
535            self.monitor_std = None;
536            self.stdx_watchdog.mark_stopped();
537        }
538    }
539
540    /// Retrieves the current resource usage metrics from `/proc`.
541    /// Returns an error if the process has exited or if `/proc` parsing fails.
542    pub async fn get_metrics(&self) -> Result<Metrics, ErrorArrayItem> {
543        self.resources.get_metrics().await
544    }
545
546    /// Returns a lock-free watchdog snapshot for the child resource monitor.
547    pub fn resource_watchdog_snapshot(&self) -> MonitorWatchdogSnapshot {
548        self.resources.watchdog_snapshot()
549    }
550
551    /// Returns a lock-free watchdog snapshot for the child stdout/stderr monitor.
552    pub fn stdx_watchdog_snapshot(&self) -> MonitorWatchdogSnapshot {
553        self.stdx_watchdog.snapshot()
554    }
555
556    /// Returns whether the child resource monitor appears healthy.
557    pub fn resource_monitor_valid(
558        &self,
559        max_staleness: Duration,
560        max_consecutive_failures: u64,
561    ) -> bool {
562        self.resources.valid(max_staleness, max_consecutive_failures)
563    }
564
565    /// Returns whether the child stdout/stderr monitor appears healthy.
566    pub fn stdx_monitor_valid(
567        &self,
568        max_staleness: Duration,
569        max_consecutive_failures: u64,
570    ) -> bool {
571        self.stdx_watchdog
572            .snapshot()
573            .is_valid(max_staleness, max_consecutive_failures)
574    }
575}
576
577impl ChildLock {
578    /// Wraps a [`Child`] in a [`LockWithTimeout`], allowing timed read/write locks on the
579    /// child handle.
580    pub fn new(child: Child) -> Self {
581        let rw_lock: LockWithTimeout<Child> = LockWithTimeout::new(child);
582        Self(rw_lock)
583    }
584
585    /// Replaces the child handle within this lock. Typically used when restarting or
586    /// re-spawning the same command.
587    pub fn update(mut self, new_child: Child) -> Self {
588        self.0 = LockWithTimeout::new(new_child);
589        self
590    }
591
592    /// Clones the internal lock (i.e., `Arc`-based duplication). This does not duplicate
593    /// the child process, only the lock mechanism that references it.
594    pub fn clone(&self) -> Self {
595        let child = &self.0;
596        let lock_clone = child.clone();
597        ChildLock { 0: lock_clone }
598    }
599
600    /// Recursively terminates the child's process group. Sends `SIGTERM` to all
601    /// descendant PIDs and then `SIGKILL` to any that remain, logging progress
602    /// at `Trace` level.
603    ///
604    /// # Errors
605    /// - Returns an [`ErrorArrayItem`] on I/O issues or if reaping fails.
606    /// - If the child’s PID is invalid, returns an error.
607    pub async fn kill(&self) -> Result<(), ErrorArrayItem> {
608        let child = self
609            .0
610            .try_read_with_timeout(Some(Duration::from_secs(5)))
611            .await?;
612
613        let xid = match child.id() {
614            Some(xid) => xid,
615            None => {
616                return Err(ErrorArrayItem::new(
617                    dusa_collection_utils::core::errors::Errors::InputOutput,
618                    "No PID found in child process".to_owned(),
619                ))
620            }
621        };
622
623        log!(LogLevel::Trace, "Killing child pid {}", xid);
624
625        if let Ok(xid) = xid.try_into() {
626            kill_pgid_recursive(xid)?;
627            Ok(())
628        } else {
629            Err(io::Error::new(io::ErrorKind::InvalidInput, "Invalid PID").into())
630        }
631    }
632
633    /// Non-blocking check for whether the child has already exited.
634    ///
635    /// This calls [`tokio::process::Child::try_wait`] under the hood, which performs
636    /// a `waitpid(..., WNOHANG)` on our behalf. Unlike a raw `kill(pid, 0)` signal
637    /// check, this correctly reports an exited process as no longer running (rather
638    /// than as a still-existing zombie), and it reaps the process in the same call
639    /// so it never lingers as a zombie.
640    ///
641    /// # Returns
642    /// - `Ok(Some(status))` if the process has already exited (now reaped).
643    /// - `Ok(None)` if the process is still running.
644    ///
645    /// # Errors
646    /// - Returns an [`ErrorArrayItem`] if the child lock can't be acquired in time.
647    pub async fn try_wait(&self) -> Result<Option<std::process::ExitStatus>, ErrorArrayItem> {
648        let mut child = self
649            .0
650            .try_write_with_timeout(Some(Duration::from_secs(1)))
651            .await?;
652        child.try_wait().map_err(ErrorArrayItem::from)
653    }
654
655    /// Checks if the child is still running -- the one true liveness check for a
656    /// process we hold a real [`tokio::process::Child`] handle for.
657    ///
658    /// This reaps via `try_wait` rather than signaling the PID directly, so an
659    /// exited-but-unreaped child (a zombie) is correctly reported as not running
660    /// instead of appearing alive.
661    ///
662    /// A lock-acquisition timeout is treated as "unknown" and reported as still
663    /// running, so callers don't tear down a healthy child on transient contention.
664    /// Any other error (e.g. the OS reporting no such child, which happens if the
665    /// process was already reaped elsewhere, such as by a concurrent `kill()`) is
666    /// treated as "not running".
667    pub async fn running(&self) -> bool {
668        match self.try_wait().await {
669            Ok(None) => true,
670            Ok(Some(_)) => false,
671            Err(err) if err.err_type == Errors::GeneralError => true,
672            Err(_) => false,
673        }
674    }
675}
676
677/// Spawns a simple child process asynchronously. Optionally captures the child's stdout/stderr,
678/// or inherits them if `capture_output` is false. Updates the application’s [`AppState`]
679/// and logs any errors.
680///
681/// # Arguments
682/// * `command` - The [`Command`] to execute.
683/// * `capture_output` - Whether to capture the child’s I/O or inherit it.
684/// * `state` - Mutable reference to an [`AppState`] for logging or state updates.
685/// * `state_path` - The location/path to which state updates are persisted.
686///
687/// # Returns
688/// - `Ok(Child)` if the process spawned successfully.
689/// - `Err(io::Error)` if spawning fails.
690///
691/// # Note
692/// - Does **not** create a new process group or call `setsid()`.
693/// - If you need a supervised child with reaping and resource monitoring,
694///   use [`spawn_complex_process`] or [`SupervisedChild::new`].
695pub async fn spawn_simple_process(
696    command: &mut Command,
697    capture_output: bool,
698    state: &mut AppState,
699    state_path: &PathType,
700) -> Result<Child, io::Error> {
701    if capture_output {
702        command.stdout(Stdio::piped());
703        command.stderr(Stdio::piped());
704    } else {
705        command.stdout(Stdio::inherit());
706        command.stderr(Stdio::inherit());
707    }
708
709    match command.spawn() {
710        Ok(child_process) => {
711            log!(
712                LogLevel::Trace,
713                "Child process spawned successfully: {:?}",
714                child_process
715            );
716            state.data = String::from("Process spawned");
717            state.event_counter += 1;
718            update_state(state, state_path, None).await;
719            Ok(child_process)
720        }
721        Err(e) => {
722            log!(
723                LogLevel::Error,
724                "Failed to spawn child process: {}",
725                e.to_string()
726            );
727            let error_item: ErrorArrayItem = ErrorArrayItem::new(
728                dusa_collection_utils::core::errors::Errors::InputOutput,
729                e.to_string(),
730            );
731            log_error(state, error_item, state_path).await;
732            Err(e)
733        }
734    }
735}
736
737/// Spawns a more complex child process that:
738/// - Optionally sets its own process group (via `setsid()` in a `pre_exec` hook),
739/// - Optionally captures stdout/stderr,
740/// - Initializes resource monitoring in [`ResourceMonitorLock`],
741/// - Wraps the process in a [`SupervisedChild`] with initialized watchdogs.
742///
743/// # Arguments
744/// * `command` - The [`Command`] to spawn.
745/// * `working_dir` - Optional path to set as the child’s current directory.
746/// * `independent_process_group` - If `true`, calls `setsid()` on spawn to isolate the process.
747/// * `capture_output` - If `true`, captures stdout/stderr; otherwise inherits them.
748///
749/// # Returns
750/// - `Ok(SupervisedChild)` containing the locked child process, resource monitor, and watchdogs.
751/// - `Err(ErrorArrayItem)` if there's an error spawning the child or initializing the monitor.
752///
753/// # Platform Details
754/// - **Linux**: `setsid()` is called in `pre_exec()` to detach from the parent's controlling terminal,
755///   giving the child a new session and making its PID the session and group leader.
756pub async fn spawn_complex_process(
757    command: &mut Command,
758    working_dir: Option<PathType>,
759    independent_process_group: bool,
760    capture_output: bool,
761) -> Result<SupervisedChild, ErrorArrayItem> {
762    log!(LogLevel::Trace, "Child to spawn: {:?}", &command);
763
764    // If we want a new process group, call setsid() in pre_exec()
765    if independent_process_group {
766        unsafe {
767            command.pre_exec(|| {
768                if libc::setsid() == -1 {
769                    return Err(io::Error::last_os_error());
770                }
771                Ok(())
772            })
773        };
774    } else {
775        command.kill_on_drop(true);
776        log!(
777            LogLevel::Trace,
778            "Complex process being spawned in the same process group"
779        );
780    }
781
782    if capture_output {
783        command.stdout(Stdio::piped());
784        command.stderr(Stdio::piped());
785    } else {
786        command.stdout(Stdio::inherit());
787        command.stderr(Stdio::inherit());
788    }
789
790    if let Some(path) = working_dir {
791        command.current_dir(path.canonicalize().map_err(ErrorArrayItem::from)?);
792    }
793
794    match command.spawn() {
795        Ok(mut child) => {
796            log!(
797                LogLevel::Trace,
798                "Child process spawned successfully: {:#?}",
799                child
800            );
801
802            let pid = match child.id() {
803                Some(d) => d,
804                None => {
805                    return Err(ErrorArrayItem::new(
806                        Errors::InputOutput,
807                        "Couldn't determine if process spawned".to_owned(),
808                    ))
809                }
810            };
811
812            let monitor = match ResourceMonitorLock::new(pid as i32) {
813                Ok(resource_monitor) => resource_monitor,
814                Err(e) => {
815                    child.kill().await?;
816                    return Err(ErrorArrayItem::from(io::Error::new(
817                        io::ErrorKind::InvalidData,
818                        e.to_string(),
819                    )));
820                }
821            };
822
823            let child = ChildLock::new(child);
824
825            Ok(SupervisedChild {
826                child,
827                resources: ResourceSupervisor {
828                    monitor,
829                    handle: None,
830                    watchdog: MonitorWatchdog::new(),
831                },
832                monitor_std: None,
833                stdout_buffer: LockWithTimeout::new(RollingBuffer::new(500)),
834                stderr_buffer: LockWithTimeout::new(RollingBuffer::new(500)),
835                stdx_watchdog: MonitorWatchdog::new(),
836            })
837        }
838        Err(error) => {
839            log!(LogLevel::Error, "Failed to spawn child process: {}", error);
840            Err(ErrorArrayItem::from(error))
841        }
842    }
843}
844
845/// Recursively collect all descendant PIDs of a given process ID, including the parent PID.
846fn collect_descendants(root_pid: i32) -> Result<HashSet<i32>, ErrorArrayItem> {
847    let mut children_map: HashMap<i32, Vec<i32>> = HashMap::new();
848    let mut result: HashSet<i32> = HashSet::new();
849
850    for prc in all_processes()
851        .map_err(|e| ErrorArrayItem::from(io::Error::new(io::ErrorKind::Other, e.to_string())))?
852    {
853        let process: Process = match prc {
854            Ok(p) => p,
855            Err(_) => continue,
856        };
857        if let Ok(stat) = process.stat() {
858            children_map
859                .entry(stat.ppid)
860                .or_default()
861                .push(process.pid());
862        }
863    }
864
865    let mut queue: VecDeque<i32> = VecDeque::new();
866    queue.push_back(root_pid);
867    result.insert(root_pid);
868
869    while let Some(pid) = queue.pop_front() {
870        if let Some(children) = children_map.get(&pid) {
871            for child in children {
872                if result.insert(*child) {
873                    queue.push_back(*child);
874                }
875            }
876        }
877    }
878
879    Ok(result)
880}
881
882/// Makes one non-blocking reap attempt (`waitpid(pid, WNOHANG)`) on a bare PID.
883///
884/// This only actually reaps anything if we're the real parent of `pid`; otherwise
885/// `waitpid` fails (typically ECHILD) and that failure is logged at `Trace` and
886/// ignored, since there's nothing we can do about a process we don't own.
887fn reap_zombie_process(pid: c_int) {
888    match waitpid(Pid::from_raw(pid), Some(WaitPidFlag::WNOHANG)) {
889        Ok(WaitStatus::Exited(_, status)) => {
890            log!(
891                LogLevel::Trace,
892                "Reaped pid {} with exit status {}",
893                pid,
894                status
895            )
896        }
897        Ok(WaitStatus::Signaled(_, sig, _)) => {
898            log!(
899                LogLevel::Trace,
900                "Reaped pid {} terminated by signal {:?}",
901                pid,
902                sig
903            )
904        }
905        Ok(WaitStatus::StillAlive) => {
906            log!(
907                LogLevel::Trace,
908                "PID {} still alive when attempting reap",
909                pid
910            )
911        }
912        Ok(status) => {
913            log!(LogLevel::Trace, "PID {} wait status: {:?}", pid, status)
914        }
915        Err(e) => {
916            log!(LogLevel::Trace, "Failed to reap pid {}: {}", pid, e)
917        }
918    }
919}
920
921/// Kill all processes belonging to a PGID and all of their descendants.
922fn kill_pgid_recursive(pgid: i32) -> Result<(), ErrorArrayItem> {
923    log!(LogLevel::Trace, "Recursively killing pgid: {}", pgid);
924    let pids = collect_descendants(pgid)?;
925    log!(LogLevel::Trace, "Found descendant pids: {:?}", pids);
926
927    for pid in &pids {
928        let res = unsafe { kill(*pid, SIGTERM) };
929        if res == 0 {
930            log!(LogLevel::Trace, "Sent SIGTERM to pid: {}", pid);
931        } else {
932            let err = io::Error::last_os_error();
933            if err.raw_os_error() == Some(libc::ESRCH) {
934                log!(LogLevel::Trace, "PID {} already exited", pid);
935            } else {
936                log!(
937                    LogLevel::Warn,
938                    "Failed to send SIGTERM to pid {}: {}",
939                    pid,
940                    err
941                );
942            }
943        }
944    }
945
946    thread::sleep(Duration::from_millis(400));
947
948    for pid in &pids {
949        reap_zombie_process(*pid);
950        if is_pid_active(*pid).unwrap_or(false) {
951            log!(LogLevel::Warn, "PID {} still running; sending SIGKILL", pid);
952            let res = unsafe { kill(*pid, SIGKILL) };
953            if res != 0 {
954                let err = io::Error::last_os_error();
955                if err.raw_os_error() != Some(libc::ESRCH) {
956                    return Err(ErrorArrayItem::from(err));
957                }
958            }
959            reap_zombie_process(*pid);
960            if !is_pid_active(*pid).unwrap_or(false) {
961                log!(LogLevel::Trace, "PID {} terminated", pid);
962            } else {
963                log!(LogLevel::Warn, "PID {} survived SIGKILL", pid);
964            }
965        } else {
966            log!(LogLevel::Trace, "PID {} terminated gracefully", pid);
967        }
968    }
969
970    Ok(())
971}
972
973/// Checks if a PID is active on the system by sending signal 0. This is a common method
974/// for detecting whether a process still exists (and if permissions allow signals).
975///
976/// # Returns
977/// - `Ok(true)` if the process exists or if we lack permissions (EPERM).
978/// - `Ok(false)` if the process does not exist (ESRCH).
979/// - `Err(io::Error)` for other system errors.
980///
981/// # Example
982/// ```rust
983/// # use artisan_middleware::process_manager::is_pid_active;
984/// match is_pid_active(1234) {
985///     Ok(true) => println!("PID 1234 is active"),
986///     Ok(false) => println!("PID 1234 is not active"),
987///     Err(e) => eprintln!("Error checking PID 1234: {}", e),
988/// }
989/// ```
990pub fn is_pid_active(pid: i32) -> io::Result<bool> {
991    // Send signal 0 to check for existence
992    let ret = unsafe { libc::kill(pid, 0) };
993    if ret == 0 {
994        // kill returned 0 => process exists or permissions are allowed
995        Ok(true)
996    } else {
997        // kill returned -1 => check errno
998        match io::Error::last_os_error().raw_os_error() {
999            Some(libc::ESRCH) => Ok(false), // No such process
1000            Some(libc::EPERM) => Ok(true),  // Process exists, but no permission
1001            Some(err) => Err(io::Error::from_raw_os_error(err)),
1002            None => Err(io::Error::new(io::ErrorKind::Other, "Unknown error")),
1003        }
1004    }
1005}
1006
1007use bytes::BytesMut;
1008
1009async fn flush_lines_to_buffer(
1010    buffer: &LockWithTimeout<RollingBuffer>,
1011    pending_lines: &mut Vec<String>,
1012) {
1013    if pending_lines.is_empty() {
1014        return;
1015    }
1016
1017    if let Ok(mut b) = buffer.try_write().await {
1018        for line in pending_lines.drain(..) {
1019            b.push(line);
1020        }
1021    }
1022}
1023
1024async fn read_stream_to_buffer<R>(
1025    mut reader: R,
1026    buffer: LockWithTimeout<RollingBuffer>,
1027    flush_interval: Duration,
1028) where
1029    R: Unpin + AsyncRead,
1030{
1031    let mut buf = BytesMut::with_capacity(1024);
1032    let mut partial = String::new();
1033    let mut pending_lines: Vec<String> = Vec::new();
1034    let mut last_flush = std::time::Instant::now();
1035
1036    loop {
1037        let remaining_until_flush = flush_interval.saturating_sub(last_flush.elapsed());
1038        match tokio::time::timeout(remaining_until_flush, reader.read_buf(&mut buf)).await {
1039            Err(_) => {
1040                flush_lines_to_buffer(&buffer, &mut pending_lines).await;
1041                last_flush = std::time::Instant::now();
1042                continue;
1043            }
1044            Ok(result) => match result {
1045                Ok(n) if n == 0 => break, // EOF
1046                Ok(_) => {}
1047                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
1048                Err(e) => {
1049                    log!(LogLevel::Warn, "Read error in stdio monitor: {}", e);
1050                    break;
1051                }
1052            },
1053        };
1054
1055        let chunk = String::from_utf8_lossy(&buf);
1056        partial.push_str(&chunk);
1057
1058        while let Some(pos) = partial.find('\n') {
1059            let line = partial[..pos].to_string();
1060            pending_lines.push(line);
1061            partial.drain(..=pos); // remove up to and including newline
1062        }
1063
1064        buf.clear();
1065        if last_flush.elapsed() >= flush_interval {
1066            flush_lines_to_buffer(&buffer, &mut pending_lines).await;
1067            last_flush = std::time::Instant::now();
1068        }
1069    }
1070
1071    // Push any trailing partial line
1072    if !partial.is_empty() {
1073        pending_lines.push(partial);
1074    }
1075    flush_lines_to_buffer(&buffer, &mut pending_lines).await;
1076}