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}