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