Skip to main content

proc_daemon/
resources.rs

1// Copyright 2023 James Gober. All rights reserved.
2// Use of this source code is governed by Apache License
3// that can be found in the LICENSE file.
4
5//! # Resource Usage Tracking
6//!
7//! This module provides functionality for tracking process resource usage,
8//! including memory and CPU utilization.
9//!
10//! ## Features
11//!
12//! - Cross-platform memory usage tracking
13//! - CPU usage monitoring with percentage calculations
14//! - Sampling at configurable intervals
15//! - Historical data collection with time-series support
16//!
17//! ## Example
18//!
19//! ```ignore
20//! use proc_daemon::resources::{ResourceTracker, ResourceUsage};
21//! use std::time::Duration;
22//!
23//! // Create a new resource tracker sampling every second
24//! let mut tracker = ResourceTracker::new(Duration::from_secs(1));
25//!
26//! // Get the current resource usage
27//! let usage = tracker.current_usage();
28//! println!("Memory: {}MB, CPU: {}%", usage.memory_mb(), usage.cpu_percent());
29//! ```
30//!
31//! With tokio runtime:
32//!
33//! ```ignore
34//! # use proc_daemon::resources::ResourceTracker;
35//! # use std::time::Duration;
36//! # let mut tracker = ResourceTracker::new(Duration::from_secs(1));
37//! #[cfg(feature = "tokio")]
38//! async {
39//!     // Start tracking
40//!     tracker.start().unwrap();
41//!     
42//!     // ... use the tracker ...
43//!     
44//!     // Stop tracking when done
45//!     tracker.stop().await;
46//! };
47//! ```
48//!
49//! With async-std runtime:
50//!
51//! ```ignore
52//! # use proc_daemon::resources::ResourceTracker;
53//! # use std::time::Duration;
54//! # let mut tracker = ResourceTracker::new(Duration::from_secs(1));
55//! #[cfg(all(feature = "async-std", not(feature = "tokio")))]
56//! async {
57//!     // Start tracking
58//!     tracker.start().unwrap();
59//!     
60//!     // ... use the tracker ...
61//!     
62//!     // Stop tracking when done
63//!     tracker.stop();
64//! };
65//! ```
66
67#[allow(unused_imports)]
68use crate::error::{Error, Result};
69#[cfg(feature = "metrics")]
70use crate::metrics::MetricsCollector;
71use arc_swap::ArcSwap;
72use std::collections::VecDeque;
73use std::sync::{Arc, RwLock};
74use std::time::{Duration, Instant};
75
76// Runtime-specific JoinHandle types
77#[cfg(all(feature = "async-std", not(feature = "tokio")))]
78#[allow(unused_imports)]
79use async_std::task::JoinHandle as AsyncJoinHandle;
80#[cfg(not(any(feature = "tokio", feature = "async-std")))]
81#[cfg(feature = "tokio")]
82#[allow(unused_imports)]
83use tokio::task::JoinHandle;
84#[cfg(feature = "tokio")]
85#[allow(unused_imports)]
86use tokio::time;
87
88// OS-specific imports
89#[cfg(target_os = "linux")]
90use std::fs::File;
91#[cfg(target_os = "linux")]
92use std::io::{BufRead, BufReader};
93#[cfg(target_os = "linux")]
94use std::num::NonZeroI64;
95
96#[cfg(target_os = "macos")]
97use std::process::Command;
98
99#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
100use windows::Win32::System::Diagnostics::ToolHelp::{
101    CreateToolhelp32Snapshot, Thread32First, Thread32Next, TH32CS_SNAPTHREAD, THREADENTRY32,
102};
103#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
104use windows::Win32::System::ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS};
105#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
106use windows::Win32::System::Threading::{GetProcessTimes, OpenProcess, PROCESS_QUERY_INFORMATION};
107
108#[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
109use windows::Win32::Foundation::{CloseHandle, FILETIME};
110
111/// Represents the current resource usage of the process
112#[derive(Debug, Clone)]
113pub struct ResourceUsage {
114    /// Timestamp when the usage was recorded
115    timestamp: Instant,
116
117    /// Memory usage in bytes
118    memory_bytes: u64,
119
120    /// CPU usage as a percentage (0-100)
121    cpu_percent: f64,
122
123    /// Number of threads in the process
124    thread_count: u32,
125}
126
127/// Monitoring alerts emitted by `ResourceTracker`.
128#[derive(Debug, Clone)]
129pub enum Alert {
130    /// Soft memory limit exceeded (informational)
131    MemorySoftLimit {
132        /// The configured soft memory limit in bytes
133        limit_bytes: u64,
134        /// The current memory usage in bytes when the alert was triggered
135        current_bytes: u64,
136    },
137}
138
139impl ResourceUsage {
140    /// Creates a new `ResourceUsage` with the current time
141    #[must_use]
142    pub fn new(memory_bytes: u64, cpu_percent: f64, thread_count: u32) -> Self {
143        Self {
144            timestamp: Instant::now(),
145            memory_bytes,
146            cpu_percent,
147            thread_count,
148        }
149    }
150
151    /// Returns the memory usage in bytes
152    #[must_use]
153    pub const fn memory_bytes(&self) -> u64 {
154        self.memory_bytes
155    }
156
157    /// Returns the memory usage in megabytes
158    #[must_use]
159    #[allow(clippy::cast_precision_loss)]
160    pub fn memory_mb(&self) -> f64 {
161        // Simplify calculation for better accuracy
162        self.memory_bytes as f64 / 1_048_576.0
163    }
164
165    /// Returns the CPU usage as a percentage (0-100)
166    #[must_use]
167    pub const fn cpu_percent(&self) -> f64 {
168        self.cpu_percent
169    }
170
171    /// Returns the number of threads in the process
172    #[must_use]
173    pub const fn thread_count(&self) -> u32 {
174        self.thread_count
175    }
176
177    /// Returns the time elapsed since this usage was recorded
178    #[must_use]
179    pub fn age(&self) -> Duration {
180        self.timestamp.elapsed()
181    }
182}
183
184/// Provides resource tracking functionality for the current process
185#[allow(dead_code)]
186pub struct ResourceTracker {
187    /// The interval at which to sample resource usage
188    sample_interval: Duration,
189
190    /// The current resource usage (lock-free reads with arc-swap)
191    current_usage: Arc<ArcSwap<ResourceUsage>>,
192
193    /// Historical usage data with timestamps
194    history: Arc<RwLock<VecDeque<ResourceUsage>>>,
195
196    /// Maximum history entries to keep
197    max_history: usize,
198
199    /// Background task handle
200    #[cfg(feature = "tokio")]
201    task_handle: Option<tokio::task::JoinHandle<()>>,
202    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
203    task_handle: Option<async_std::task::JoinHandle<()>>,
204    #[cfg(not(any(feature = "tokio", feature = "async-std")))]
205    task_handle: Option<std::thread::JoinHandle<()>>,
206
207    /// The process ID being monitored (usually self)
208    pid: u32,
209
210    /// Optional soft memory limit in bytes. If exceeded, an alert is emitted.
211    memory_soft_limit_bytes: Option<u64>,
212
213    /// Optional alert handler callback
214    #[allow(clippy::type_complexity)]
215    on_alert: Option<Arc<dyn Fn(Alert) + Send + Sync + 'static>>,
216
217    /// Optional metrics collector (feature-gated)
218    #[cfg(feature = "metrics")]
219    metrics: Option<MetricsCollector>,
220}
221
222impl ResourceTracker {
223    /// Creates a new `ResourceTracker` with the given sampling interval.
224    ///
225    /// # Security
226    ///
227    /// By default, tracks the current process. To track a different process,
228    /// use `with_pid()` but note this may fail if the process doesn't exist
229    /// or you lack permissions.
230    #[must_use]
231    pub fn new(sample_interval: Duration) -> Self {
232        // Initialize with default values
233        let initial_usage = ResourceUsage::new(0, 0.0, 0);
234        let current_pid = std::process::id();
235
236        Self {
237            sample_interval,
238            current_usage: Arc::new(ArcSwap::from_pointee(initial_usage)),
239            history: Arc::new(RwLock::new(VecDeque::new())),
240            max_history: 60, // Default to 1 minute at 1 second intervals
241            task_handle: None,
242            pid: current_pid,
243            memory_soft_limit_bytes: None,
244            on_alert: None,
245            #[cfg(feature = "metrics")]
246            metrics: None,
247        }
248    }
249
250    /// Set a specific PID to monitor (defaults to current process).
251    ///
252    /// # Security
253    ///
254    /// Monitoring arbitrary processes may fail due to OS permissions.
255    /// Only use this if you have explicit authorization.
256    #[must_use]
257    pub const fn with_pid(mut self, pid: u32) -> Self {
258        self.pid = pid;
259        self
260    }
261
262    /// Sets the maximum history entries to keep
263    #[must_use]
264    pub const fn with_max_history(mut self, max_entries: usize) -> Self {
265        self.max_history = max_entries;
266        self
267    }
268
269    /// Sets a soft memory limit in bytes. When exceeded, an alert is emitted via `on_alert`.
270    #[must_use]
271    pub const fn with_memory_soft_limit_bytes(mut self, bytes: u64) -> Self {
272        self.memory_soft_limit_bytes = Some(bytes);
273        self
274    }
275
276    /// Sets an alert handler callback for monitoring alerts.
277    #[must_use]
278    pub fn with_alert_handler<F>(mut self, f: F) -> Self
279    where
280        F: Fn(Alert) + Send + Sync + 'static,
281    {
282        self.on_alert = Some(Arc::new(f));
283        self
284    }
285
286    /// Attaches a metrics collector for reporting resource metrics.
287    #[cfg(feature = "metrics")]
288    #[must_use]
289    pub fn with_metrics(mut self, metrics: MetricsCollector) -> Self {
290        self.metrics = Some(metrics);
291        self
292    }
293
294    /// Convenience: route alerts to tracing logs.
295    ///
296    /// Logs as `tracing::warn!` with structured fields per alert type.
297    #[must_use]
298    pub fn with_alert_to_tracing(mut self) -> Self {
299        self.on_alert = Some(Arc::new(|alert| match alert {
300            Alert::MemorySoftLimit {
301                limit_bytes,
302                current_bytes,
303            } => {
304                tracing::warn!(
305                    target: "proc_daemon::resources",
306                    limit_bytes,
307                    current_bytes,
308                    "Resource alert: soft memory limit exceeded"
309                );
310            }
311        }));
312        self
313    }
314
315    /// Starts the resource tracking in the background
316    /// Starts the resource tracker's background sampling task
317    ///
318    /// # Errors
319    ///
320    /// Starts the resource tracker background task.
321    ///
322    /// Returns an error if the process ID cannot be determined or
323    /// if there's an issue with the system APIs when gathering resource metrics.
324    ///
325    /// # Panics
326    ///
327    /// May panic if the `RwLock` for history is poisoned in an extremely rare concurrent scenario.
328    /// This is a safety guarantee but should not happen in normal operation due to panic guards.
329    #[cfg(all(feature = "tokio", not(feature = "async-std")))]
330    pub fn start(&mut self) -> Result<()> {
331        if self.task_handle.is_some() {
332            return Ok(()); // Already started
333        }
334
335        let sample_interval = self.sample_interval;
336        let usage_history = Arc::clone(&self.history);
337        let current_usage = Arc::clone(&self.current_usage);
338        let pid = self.pid;
339        let max_history = self.max_history;
340        let memory_soft_limit_bytes = self.memory_soft_limit_bytes;
341        let on_alert = self.on_alert.clone();
342        #[cfg(feature = "metrics")]
343        let metrics = self.metrics.clone();
344
345        let handle = tokio::spawn(async move {
346            let mut interval_timer = time::interval(sample_interval);
347            let mut last_cpu_time = 0.0;
348            let mut last_timestamp = Instant::now();
349            #[cfg(feature = "metrics")]
350            let mut last_tick = Instant::now();
351
352            loop {
353                interval_timer.tick().await;
354                #[cfg(feature = "metrics")]
355                let tick_now = Instant::now();
356
357                // Get current resource usage
358                if let Ok(usage) =
359                    Self::sample_resource_usage(pid, &mut last_cpu_time, &mut last_timestamp)
360                {
361                    // Update current usage (lock-free store)
362                    current_usage.store(Arc::new(usage.clone()));
363
364                    // Update history with minimal lock time
365                    {
366                        let mut hist = usage_history.write().unwrap();
367                        hist.push_back(usage.clone());
368                        // Trim excess entries in one loop
369                        while hist.len() > max_history {
370                            hist.pop_front();
371                        }
372                        drop(hist); // Explicitly drop lock
373                    }
374
375                    // Soft memory limit alert
376                    if let Some(limit) = memory_soft_limit_bytes {
377                        if usage.memory_bytes() > limit {
378                            if let Some(cb) = on_alert.as_ref() {
379                                cb(Alert::MemorySoftLimit {
380                                    limit_bytes: limit,
381                                    current_bytes: usage.memory_bytes(),
382                                });
383                            }
384                        }
385                    }
386
387                    // Metrics reporting (feature-gated)
388                    #[cfg(feature = "metrics")]
389                    if let Some(m) = metrics.as_ref() {
390                        m.set_gauge("proc.memory_bytes", usage.memory_bytes());
391                        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
392                        let cpu_milli = (usage.cpu_percent() * 1000.0).max(0.0).round() as u64;
393                        m.set_gauge("proc.cpu_milli_percent", cpu_milli);
394                        m.set_gauge("proc.thread_count", u64::from(usage.thread_count()));
395                        m.increment_counter("proc.samples_total", 1);
396                        m.record_histogram(
397                            "proc.sample_interval",
398                            tick_now.saturating_duration_since(last_tick),
399                        );
400                        last_tick = tick_now;
401                    }
402                }
403            }
404        });
405
406        self.task_handle = Some(handle);
407        Ok(())
408    }
409
410    /// Starts the resource tracking
411    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
412    #[allow(clippy::missing_errors_doc)]
413    pub fn start(&mut self) -> Result<()> {
414        if self.task_handle.is_some() {
415            return Ok(()); // Already started
416        }
417
418        let sample_interval = self.sample_interval;
419        let usage_history = Arc::clone(&self.history);
420        let current_usage = Arc::clone(&self.current_usage);
421        let pid = self.pid;
422        let max_history = self.max_history; // Clone max_history to use inside async block
423        let memory_soft_limit_bytes = self.memory_soft_limit_bytes;
424        let on_alert = self.on_alert.clone();
425        #[cfg(feature = "metrics")]
426        let metrics = self.metrics.clone();
427
428        let handle = async_std::task::spawn(async move {
429            let mut last_cpu_time = 0.0;
430            let mut last_timestamp = Instant::now();
431            #[cfg(feature = "metrics")]
432            let mut last_tick = Instant::now();
433
434            loop {
435                async_std::task::sleep(sample_interval).await;
436                #[cfg(feature = "metrics")]
437                let tick_now = Instant::now();
438
439                // Get current resource usage
440                if let Ok(usage) =
441                    Self::sample_resource_usage(pid, &mut last_cpu_time, &mut last_timestamp)
442                {
443                    // Update current usage (lock-free store via ArcSwap)
444                    // Reuse Arc allocation by swapping instead of always allocating new
445                    let new_arc = Arc::new(usage.clone());
446                    current_usage.store(new_arc);
447
448                    // Update history with minimal lock time
449                    {
450                        let mut hist = usage_history.write().unwrap();
451                        hist.push_back(usage.clone());
452                        // Trim excess entries in one loop
453                        while hist.len() > max_history {
454                            hist.pop_front();
455                        }
456                    } // Drop lock immediately
457
458                    // Soft memory limit alert
459                    if let Some(limit) = memory_soft_limit_bytes {
460                        if usage.memory_bytes() > limit {
461                            if let Some(cb) = on_alert.as_ref() {
462                                cb(Alert::MemorySoftLimit {
463                                    limit_bytes: limit,
464                                    current_bytes: usage.memory_bytes(),
465                                });
466                            }
467                        }
468                    }
469
470                    // Metrics reporting (feature-gated)
471                    #[cfg(feature = "metrics")]
472                    if let Some(m) = metrics.as_ref() {
473                        m.set_gauge("proc.memory_bytes", usage.memory_bytes());
474                        let cpu_milli = (usage.cpu_percent() * 1000.0).max(0.0).round() as u64;
475                        m.set_gauge("proc.cpu_milli_percent", cpu_milli);
476                        m.set_gauge("proc.thread_count", u64::from(usage.thread_count()));
477                        m.increment_counter("proc.samples_total", 1);
478                        m.record_histogram(
479                            "proc.sample_interval",
480                            tick_now.saturating_duration_since(last_tick),
481                        );
482                    }
483                    #[cfg(feature = "metrics")]
484                    {
485                        last_tick = tick_now;
486                    }
487                }
488            }
489        });
490
491        self.task_handle = Some(handle);
492        Ok(())
493    }
494
495    /// Stops the resource tracker, cancelling any ongoing monitoring task.
496    ///
497    /// For tokio, this aborts the task and awaits its completion.
498    #[cfg(all(feature = "tokio", not(feature = "async-std")))]
499    pub async fn stop(&mut self) {
500        if let Some(handle) = self.task_handle.take() {
501            handle.abort();
502            let _ = handle.await;
503        }
504    }
505
506    /// Stops the resource tracker, cancelling any ongoing monitoring task.
507    ///
508    /// For async-std, this simply drops the `JoinHandle` which cancels the task.
509    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
510    pub fn stop(&mut self) {
511        // Just drop the handle, which will cancel the task on async-std
512        self.task_handle.take();
513    }
514
515    /// Returns the current resource usage
516    #[must_use]
517    pub fn current_usage(&self) -> ResourceUsage {
518        self.current_usage.load_full().as_ref().clone()
519    }
520
521    /// Returns a copy of the resource usage history
522    #[must_use]
523    pub fn history(&self) -> Vec<ResourceUsage> {
524        self.history
525            .read()
526            .map_or_else(|_| Vec::new(), |history| history.iter().cloned().collect())
527    }
528
529    /// Samples the resource usage for the given process ID
530    #[allow(unused_variables, dead_code)]
531    #[allow(clippy::needless_pass_by_ref_mut)]
532    fn sample_resource_usage(
533        pid: u32,
534        last_cpu_time: &mut f64,
535        last_timestamp: &mut Instant,
536    ) -> Result<ResourceUsage> {
537        #[cfg(target_os = "linux")]
538        {
539            // On Linux, read from /proc filesystem
540            let memory = Self::get_memory_linux(pid)?;
541            let (cpu, threads) = Self::get_cpu_linux(pid, last_cpu_time, last_timestamp)?;
542            Ok(ResourceUsage::new(memory, cpu, threads))
543        }
544
545        #[cfg(target_os = "macos")]
546        {
547            // On macOS, use ps command
548            let memory = Self::get_memory_macos(pid)?;
549            let (cpu, threads) = Self::get_cpu_macos(pid)?;
550            Ok(ResourceUsage::new(memory, cpu, threads))
551        }
552
553        #[cfg(target_os = "windows")]
554        {
555            // On Windows, use Windows API
556            let memory = Self::get_memory_windows(pid)?;
557            let (cpu, threads) = Self::get_cpu_windows(pid, last_cpu_time, last_timestamp)?;
558            Ok(ResourceUsage::new(memory, cpu, threads))
559        }
560
561        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
562        {
563            // Default placeholder for unsupported platforms
564            Ok(ResourceUsage::new(0, 0.0, 0))
565        }
566    }
567
568    #[cfg(target_os = "linux")]
569    fn get_memory_linux(pid: u32) -> Result<u64> {
570        // Read memory information from /proc/[pid]/status
571        let path = format!("/proc/{pid}/status");
572        let file = File::open(&path).map_err(|e| {
573            Error::io_with_source(format!("Failed to open {path} for memory stats"), e)
574        })?;
575
576        let reader = BufReader::new(file);
577        let mut memory_bytes = 0;
578
579        for line in reader.lines() {
580            let line = line.map_err(|e| {
581                Error::io_with_source("Failed to read process memory stats".to_string(), e)
582            })?;
583
584            // VmRSS gives the resident set size
585            if line.starts_with("VmRSS:") {
586                let parts: Vec<&str> = line.split_whitespace().collect();
587                if !parts.is_empty() {
588                    if let Ok(kb) = parts[1].parse::<u64>() {
589                        memory_bytes = kb * 1024;
590                        break;
591                    }
592                }
593            }
594        }
595
596        Ok(memory_bytes)
597    }
598
599    #[cfg(target_os = "linux")]
600    #[allow(
601        clippy::cast_precision_loss,
602        clippy::cast_possible_truncation,
603        clippy::similar_names
604    )]
605    fn get_cpu_linux(
606        pid: u32,
607        last_cpu_time: &mut f64,
608        last_timestamp: &mut Instant,
609    ) -> Result<(f64, u32)> {
610        // Read CPU information from /proc/[pid]/stat
611        let path = format!("/proc/{pid}/stat");
612        let file = File::open(&path).map_err(|e| {
613            Error::io_with_source(format!("Failed to open {path} for CPU stats"), e)
614        })?;
615
616        let reader = BufReader::new(file);
617        let mut cpu_percent = 0.0;
618        let mut thread_count: u32 = 0;
619
620        if let Ok(line) = reader.lines().next().ok_or_else(|| {
621            Error::runtime("Failed to read CPU stats from proc filesystem".to_string())
622        }) {
623            let line = line.map_err(|e| {
624                Error::io_with_source("Failed to read process CPU stats".to_string(), e)
625            })?;
626
627            if let Some((cpu_time, threads)) = Self::parse_proc_stat(&line) {
628                thread_count = threads;
629
630                let now = Instant::now();
631                if *last_timestamp != now {
632                    let time_diff = now.duration_since(*last_timestamp).as_secs_f64();
633                    if time_diff > 0.0 {
634                        let num_cores = num_cpus::get() as f64;
635                        let cpu_time_diff = cpu_time - *last_cpu_time;
636                        let ticks = Self::linux_clk_tck();
637
638                        cpu_percent = (cpu_time_diff / ticks) / time_diff * 100.0 / num_cores;
639                    }
640                }
641
642                *last_cpu_time = cpu_time;
643                *last_timestamp = now;
644            }
645        }
646
647        Ok((cpu_percent, thread_count))
648    }
649
650    #[cfg(target_os = "linux")]
651    fn parse_proc_stat(line: &str) -> Option<(f64, u32)> {
652        let open = line.find('(')?;
653        let close = line.rfind(')')?;
654        if close <= open {
655            return None;
656        }
657
658        // Everything after the closing ')' begins with state (field 3).
659        let rest = line.get((close + 1)..)?;
660        let parts: Vec<&str> = rest.split_whitespace().collect();
661        // Need up to field 20 (num_threads) => index 17 in this slice.
662        if parts.len() <= 17 {
663            return None;
664        }
665
666        let utime = parts.get(11)?.parse::<f64>().unwrap_or(0.0);
667        let stime = parts.get(12)?.parse::<f64>().unwrap_or(0.0);
668        let child_user_time = parts.get(13)?.parse::<f64>().unwrap_or(0.0);
669        let child_system_time = parts.get(14)?.parse::<f64>().unwrap_or(0.0);
670        let thread_count = parts.get(17)?.parse::<u32>().unwrap_or(0);
671
672        let current_cpu_time = utime + stime + child_user_time + child_system_time;
673        Some((current_cpu_time, thread_count))
674    }
675
676    #[cfg(target_os = "linux")]
677    #[allow(clippy::cast_precision_loss)]
678    #[allow(unsafe_code)]
679    fn linux_clk_tck() -> f64 {
680        // SAFETY: This is a read-only system configuration query that's guaranteed to be safe.
681        // sysconf(_SC_CLK_TCK) returns the number of clock ticks per second, which is a
682        // system constant that cannot cause memory safety issues.
683        #[cfg_attr(not(target_os = "linux"), allow(unused_unsafe))]
684        let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
685        NonZeroI64::new(ticks).map_or(100.0, |v| v.get() as f64)
686    }
687
688    #[cfg(target_os = "macos")]
689    #[allow(dead_code)]
690    fn get_memory_macos(pid: u32) -> Result<u64> {
691        // Use ps command to get memory usage on macOS
692        let output = Command::new("/bin/ps")
693            .args(["-xo", "rss=", "-p", &pid.to_string()])
694            .output()
695            .map_err(|e| {
696                Error::io_with_source(
697                    "Failed to execute ps command for memory stats".to_string(),
698                    e,
699                )
700            })?;
701
702        let memory_kb = String::from_utf8_lossy(&output.stdout)
703            .trim()
704            .parse::<u64>()
705            .unwrap_or(0);
706
707        Ok(memory_kb * 1024)
708    }
709
710    #[cfg(target_os = "macos")]
711    #[allow(dead_code)]
712    fn get_cpu_macos(pid: u32) -> Result<(f64, u32)> {
713        // Get CPU percentage using ps
714        let output = Command::new("/bin/ps")
715            .args(["-xo", "%cpu,thcount=", "-p", &pid.to_string()])
716            .output()
717            .map_err(|e| {
718                Error::io_with_source("Failed to execute ps command for CPU stats".to_string(), e)
719            })?;
720
721        let stats = String::from_utf8_lossy(&output.stdout);
722        let parts: Vec<&str> = stats.split_whitespace().collect();
723
724        let cpu_percent = if parts.is_empty() {
725            0.0
726        } else {
727            parts[0].parse::<f64>().unwrap_or(0.0)
728        };
729
730        let thread_count = if parts.len() > 1 {
731            parts[1].parse::<u32>().unwrap_or(0)
732        } else {
733            0
734        };
735
736        Ok((cpu_percent, thread_count))
737    }
738
739    #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
740    #[allow(unsafe_code)]
741    fn get_memory_windows(pid: u32) -> Result<u64> {
742        let mut pmc = PROCESS_MEMORY_COUNTERS::default();
743        let handle =
744            unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, false, pid) }.map_err(|e| {
745                Error::runtime_with_source(
746                    format!("Failed to open process {} for memory stats", pid),
747                    e,
748                )
749            })?;
750
751        let result = unsafe {
752            GetProcessMemoryInfo(
753                handle,
754                &mut pmc,
755                std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32,
756            )
757        }
758        .map_err(|e| {
759            Error::runtime_with_source("Failed to get process memory info".to_string(), e)
760        });
761
762        unsafe { CloseHandle(handle) };
763        result?;
764
765        Ok(u64::try_from(pmc.WorkingSetSize).unwrap_or(pmc.WorkingSetSize as u64))
766    }
767
768    #[cfg(all(target_os = "windows", not(feature = "windows-monitoring")))]
769    fn get_memory_windows(_pid: u32) -> Result<u64> {
770        Err(Error::runtime(
771            "Windows monitoring not enabled. Enable the 'windows-monitoring' feature".to_string(),
772        ))
773    }
774
775    #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
776    #[allow(unsafe_code, clippy::cast_precision_loss)]
777    fn get_cpu_windows(
778        pid: u32,
779        last_cpu_time: &mut f64,
780        last_timestamp: &mut Instant,
781    ) -> Result<(f64, u32)> {
782        let mut cpu_percent = 0.0;
783        let mut thread_count = 0;
784
785        let handle =
786            unsafe { OpenProcess(PROCESS_QUERY_INFORMATION, false, pid) }.map_err(|e| {
787                Error::runtime_with_source(
788                    format!("Failed to open process {} for CPU stats", pid),
789                    e,
790                )
791            })?;
792
793        // Get thread count by enumerating threads using ToolHelp snapshot
794        unsafe {
795            let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0).map_err(|e| {
796                Error::runtime_with_source(
797                    "Failed to create ToolHelp snapshot for threads".to_string(),
798                    e,
799                )
800            })?;
801
802            let mut entry: THREADENTRY32 = std::mem::zeroed();
803            entry.dwSize = std::mem::size_of::<THREADENTRY32>() as u32;
804
805            if Thread32First(snapshot, &mut entry).is_ok() {
806                loop {
807                    if entry.th32OwnerProcessID == pid {
808                        thread_count += 1;
809                    }
810                    if Thread32Next(snapshot, &mut entry).is_err() {
811                        break;
812                    }
813                }
814            }
815
816            let _ = CloseHandle(snapshot);
817        }
818
819        // Get CPU times
820        let mut creation_time = FILETIME::default();
821        let mut exit_time = FILETIME::default();
822        let mut kernel_time = FILETIME::default();
823        let mut user_time = FILETIME::default();
824
825        let result = unsafe {
826            GetProcessTimes(
827                handle,
828                &mut creation_time,
829                &mut exit_time,
830                &mut kernel_time,
831                &mut user_time,
832            )
833        };
834
835        if result.is_ok() {
836            let kernel_ns = Self::filetime_to_ns(&kernel_time);
837            let user_ns = Self::filetime_to_ns(&user_time);
838            let total_time = (kernel_ns + user_ns) as f64 / 1_000_000_000.0; // Convert to seconds
839
840            let now = Instant::now();
841            if *last_timestamp != now {
842                let time_diff = now.duration_since(*last_timestamp).as_secs_f64();
843                if time_diff > 0.0 {
844                    let time_diff_cpu = total_time - *last_cpu_time;
845                    let num_cores = num_cpus::get() as f64;
846
847                    // Calculate CPU percentage
848                    cpu_percent = (time_diff_cpu / time_diff) * 100.0 / num_cores;
849                }
850            }
851
852            // Update last values
853            *last_cpu_time = total_time;
854            *last_timestamp = now;
855        }
856
857        unsafe { CloseHandle(handle) };
858
859        Ok((cpu_percent, thread_count))
860    }
861
862    #[cfg(all(target_os = "windows", not(feature = "windows-monitoring")))]
863    fn get_cpu_windows(
864        _pid: u32,
865        _last_cpu_time: &mut f64,
866        _last_timestamp: &mut Instant,
867    ) -> Result<(f64, u32)> {
868        Err(Error::runtime(
869            "Windows monitoring not enabled. Enable the 'windows-monitoring' feature".to_string(),
870        ))
871    }
872
873    #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
874    fn filetime_to_ns(ft: &windows::Win32::Foundation::FILETIME) -> u64 {
875        // Convert Windows FILETIME to nanoseconds
876        let high = (ft.dwHighDateTime as u64) << 32;
877        let low = ft.dwLowDateTime as u64;
878        // Windows ticks are 100ns intervals
879        (high + low) * 100
880    }
881}
882
883impl Drop for ResourceTracker {
884    fn drop(&mut self) {
885        if let Some(handle) = self.task_handle.take() {
886            #[cfg(feature = "tokio")]
887            handle.abort();
888            // For async-std, dropping the handle is sufficient
889            // as it cancels the associated task
890            #[cfg(all(feature = "async-std", not(feature = "tokio")))]
891            drop(handle); // Explicitly drop handle to ensure it's used
892        }
893    }
894}
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899    use std::time::Duration;
900
901    #[cfg(feature = "tokio")]
902    #[cfg_attr(miri, ignore)]
903    #[tokio::test]
904    async fn test_resource_tracker_creation() {
905        let tracker = ResourceTracker::new(Duration::from_secs(1));
906        assert_eq!(tracker.max_history, 60);
907        assert_eq!(tracker.sample_interval, Duration::from_secs(1));
908    }
909
910    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
911    #[async_std::test]
912    async fn test_resource_tracker_creation() {
913        let tracker = ResourceTracker::new(Duration::from_secs(1));
914        assert_eq!(tracker.max_history, 60);
915        assert_eq!(tracker.sample_interval, Duration::from_secs(1));
916    }
917
918    #[cfg(feature = "tokio")]
919    #[cfg_attr(miri, ignore)]
920    #[tokio::test]
921    async fn test_resource_usage_methods() {
922        let usage = ResourceUsage::new(1_048_576, 5.5, 4);
923        assert_eq!(usage.memory_bytes(), 1_048_576);
924        // Use a more reasonable epsilon for floating point comparisons
925        let epsilon: f64 = 1e-6;
926        assert!((usage.memory_mb() - 1.0).abs() < epsilon);
927        assert!((usage.cpu_percent() - 5.5).abs() < epsilon);
928        assert_eq!(usage.thread_count(), 4);
929        assert!(usage.age() >= Duration::from_nanos(0));
930    }
931
932    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
933    #[async_std::test]
934    async fn test_resource_usage_methods() {
935        let usage = ResourceUsage::new(1_048_576, 5.5, 4);
936        assert_eq!(usage.memory_bytes(), 1_048_576);
937        // Use a more reasonable epsilon for floating point comparisons
938        let epsilon: f64 = 1e-6;
939        assert!((usage.memory_mb() - 1.0).abs() < epsilon);
940        assert!((usage.cpu_percent() - 5.5).abs() < epsilon);
941        assert_eq!(usage.thread_count(), 4);
942        assert!(usage.age() >= Duration::from_nanos(0));
943    }
944
945    #[cfg(feature = "tokio")]
946    #[cfg_attr(miri, ignore)]
947    #[tokio::test]
948    async fn test_tracker_with_max_history() {
949        let tracker = ResourceTracker::new(Duration::from_secs(1)).with_max_history(100);
950        assert_eq!(tracker.max_history, 100);
951    }
952
953    #[cfg(all(target_os = "windows", feature = "windows-monitoring"))]
954    #[cfg_attr(miri, ignore)]
955    #[test]
956    fn test_windows_toolhelp_thread_count_path() {
957        let pid = std::process::id();
958        let mut last_cpu_time = 0.0;
959        let mut last_timestamp = Instant::now();
960
961        // Exercise the ToolHelp-based sampling path
962        let usage = ResourceTracker::sample_resource_usage(
963            pid,
964            &mut last_cpu_time,
965            &mut last_timestamp,
966        )
967        .expect(
968            "Windows sample_resource_usage should succeed with windows-monitoring feature enabled",
969        );
970
971        // A running process should have at least one thread
972        assert!(
973            usage.thread_count() >= 1,
974            "expected at least 1 thread, got {}",
975            usage.thread_count()
976        );
977    }
978
979    #[cfg(all(feature = "async-std", not(feature = "tokio")))]
980    #[async_std::test]
981    async fn test_tracker_with_max_history() {
982        let tracker = ResourceTracker::new(Duration::from_secs(1)).with_max_history(100);
983        assert_eq!(tracker.max_history, 100);
984    }
985}