Skip to main content

fuel_telemetry/
process_watcher.rs

1use crate::{
2    self as fuel_telemetry, daemonise,
3    errors::{into_recoverable, TelemetryError},
4    get_process_name, info_telemetry, span_telemetry, telemetry_config, telemetry_formatter,
5    EnvSetting, Result, WatcherResult,
6};
7use nix::{
8    sys::signal::kill,
9    unistd::{getpid, Pid as NixPid},
10};
11use std::{
12    env::{set_var, var},
13    fs::OpenOptions,
14    io::Write,
15    path::{Path, PathBuf},
16    process::exit,
17    sync::{
18        atomic::{AtomicBool, AtomicI32, Ordering},
19        LazyLock,
20    },
21    thread::sleep,
22    time::{Duration, Instant},
23};
24use sysinfo::{Pid as SysinfoPid, ProcessRefreshKind, ProcessesToUpdate, System};
25use tracing_subscriber::{prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt};
26
27//
28// Module config
29//
30
31const PROCESS_NAME: &str = "telemetry-process-watcher";
32const PROCESSWATCHER_CHECK_INTERVAL: &str = "1";
33const PROCESSWATCHER_MEASURE_INTERVAL: &str = "60";
34const TELEMETRY_PKG_NAME: &str = "process_watcher";
35
36struct ProcessWatcherConfig {
37    // The path to its logfile
38    logfile: PathBuf,
39    // The interval between checking if the process is still alive (in seconds)
40    check_interval: Duration,
41    // The interval between measuring process metrics (in seconds)
42    measure_interval: Duration,
43}
44
45fn config() -> Result<&'static ProcessWatcherConfig> {
46    static PROCESS_WATCHER_CONFIG: LazyLock<Result<ProcessWatcherConfig>> = LazyLock::new(|| {
47        let get_env = |key, default| EnvSetting::new(key, default).get();
48
49        let check_interval = get_env(
50            "PROCESSWATCHER_CHECK_INTERVAL",
51            PROCESSWATCHER_CHECK_INTERVAL,
52        )
53        .parse()
54        .map(Duration::from_secs)?;
55
56        let measure_interval = get_env(
57            "PROCESSWATCHER_MEASURE_INTERVAL",
58            PROCESSWATCHER_MEASURE_INTERVAL,
59        )
60        .parse()
61        .map(Duration::from_secs)?;
62
63        Ok(ProcessWatcherConfig {
64            check_interval,
65            measure_interval,
66            logfile: Path::new(&telemetry_config()?.fuelup_log).join(format!("{PROCESS_NAME}.log")),
67        })
68    });
69
70    PROCESS_WATCHER_CONFIG
71        .as_ref()
72        .map_err(|e| TelemetryError::InvalidConfig(e.to_string()))
73}
74
75pub struct ProcessWatcher {
76    // The name of the process used as the "measurement" for InfluxDB
77    exe_name: String,
78
79    // The PID we are watching (Nix PID)
80    pid_to_watch_nix: NixPid,
81    // The PID we are watching (Sysinfo PID)
82    pid_to_watch_sysinfo: SysinfoPid,
83
84    // The timer for checking if the process is still alive
85    check_timer: Timer,
86    // The timer for measuring the process metrics
87    measure_timer: Timer,
88
89    // Max metrics values we've seen so far
90    resident_memory: u64,
91    virtual_memory: u64,
92    run_time: u64,
93    cpu_usage: f32,
94}
95
96// Prevent recursive calls to start()
97static STARTED: AtomicBool = AtomicBool::new(false);
98
99/// The PID of the currently running `ProcessWatcher` daemon
100pub static PID: AtomicI32 = AtomicI32::new(0);
101
102impl ProcessWatcher {
103    pub fn new() -> Result<Self> {
104        Self::new_with_pid(getpid())
105    }
106
107    pub fn new_with_pid(pid_to_watch: NixPid) -> Result<Self> {
108        Ok(Self {
109            exe_name: get_process_name(),
110
111            pid_to_watch_nix: pid_to_watch,
112            pid_to_watch_sysinfo: SysinfoPid::from(pid_to_watch.as_raw() as usize),
113
114            check_timer: Timer::new(config()?.check_interval),
115            measure_timer: Timer::new(config()?.measure_interval),
116
117            resident_memory: 0,
118            virtual_memory: 0,
119            run_time: 0,
120            cpu_usage: 0.0,
121        })
122    }
123
124    pub fn start(&mut self) -> WatcherResult<()> {
125        self.start_with_helpers(&DefaultStartHelpers)
126    }
127
128    fn start_with_helpers(&mut self, helpers: &impl StartHelpers) -> WatcherResult<()> {
129        if var("FUELUP_NO_TELEMETRY").is_ok() {
130            // If telemetry is disabled, immediately return
131            return Ok(());
132        }
133
134        let logfile = &config().map_err(into_recoverable)?.logfile;
135
136        if STARTED.load(Ordering::Relaxed) {
137            return Ok(());
138        } else {
139            STARTED.store(true, Ordering::Relaxed);
140        }
141
142        match helpers.daemonise(logfile) {
143            Ok(Some(pid)) => {
144                // We are the parent, so record the PID then immediately return
145                PID.store(pid.as_raw(), Ordering::Relaxed);
146                return Ok(());
147            }
148            Err(e) => {
149                // Couldn't daemonise, so clear globals before returning the error
150                STARTED.store(false, Ordering::Relaxed);
151                PID.store(0, Ordering::Relaxed);
152                return Err(e);
153            }
154            Ok(None) => {
155                // We are the child process, so continue as the `ProcessWatcher`
156                PID.store(getpid().as_raw(), Ordering::Relaxed);
157            }
158        }
159
160        set_var("TELEMETRY_PKG_NAME", TELEMETRY_PKG_NAME);
161        let (telemetry_layer, _guard) = helpers.new_fuel_telemetry()?;
162        tracing_subscriber::registry().with(telemetry_layer).init();
163
164        // Take a measurement now before going to sleep
165        helpers.measure_process(self);
166
167        while helpers.process_is_alive(self) {
168            self.check_timer.last_measure = helpers.get_instant_now();
169
170            if helpers.measure_timer_is_ready(self) {
171                helpers.measure_process(self);
172            }
173
174            let next_ready_timer =
175                helpers.get_next_ready_timer(&[&self.check_timer, &self.measure_timer]);
176
177            helpers.sleep(next_ready_timer.duration_until_ready());
178        }
179
180        // Record metrics one last time
181        helpers.record_metrics(self, false);
182
183        helpers.exit(0);
184    }
185
186    /// Last resort logging of errors, only to be used by the caller when there
187    /// is no other way to report an error.
188    pub fn log_error(message: &str) -> Result<()> {
189        let mut file = OpenOptions::new()
190            .create(true)
191            .append(true)
192            .open(&config()?.logfile)?;
193
194        Ok(writeln!(file, "{message}")?)
195    }
196}
197
198trait StartHelpers {
199    fn daemonise(&self, logfile: &PathBuf) -> WatcherResult<Option<NixPid>> {
200        daemonise(logfile)
201    }
202
203    #[allow(clippy::type_complexity)]
204    fn new_fuel_telemetry(
205        &self,
206    ) -> Result<(
207        tracing_subscriber::filter::Filtered<
208            tracing_subscriber::fmt::Layer<
209                tracing_subscriber::Registry,
210                tracing_subscriber::fmt::format::DefaultFields,
211                telemetry_formatter::TelemetryFormatter,
212                tracing_appender::non_blocking::NonBlocking,
213            >,
214            tracing_subscriber::EnvFilter,
215            tracing_subscriber::Registry,
216        >,
217        tracing_appender::non_blocking::WorkerGuard,
218    )> {
219        fuel_telemetry::new!()
220    }
221
222    fn measure_process(&self, process_watcher: &mut ProcessWatcher) {
223        let mut sysinfo = System::new();
224
225        // Need to refresh twice with delay in between so that there are enough
226        // data points for "cpu_usage". See `sysinfo` docs for more details.
227        sysinfo.refresh_processes_specifics(
228            ProcessesToUpdate::Some(&[process_watcher.pid_to_watch_sysinfo]),
229            true,
230            ProcessRefreshKind::nothing().with_memory().with_cpu(),
231        );
232
233        sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
234
235        sysinfo.refresh_processes_specifics(
236            ProcessesToUpdate::Some(&[process_watcher.pid_to_watch_sysinfo]),
237            true,
238            ProcessRefreshKind::nothing().with_memory().with_cpu(),
239        );
240
241        let Some(process) = sysinfo.process(process_watcher.pid_to_watch_sysinfo) else {
242            // Process has likely exited, so return and deal with it at a higher level
243            return;
244        };
245
246        // Store the metrics so we can find the max values for the entire run
247        let residential_memory = process.memory();
248        let virtual_memory = process.virtual_memory();
249        let run_time = process.run_time();
250        let cpu_usage = process.cpu_usage();
251
252        process_watcher.resident_memory = process_watcher.resident_memory.max(residential_memory);
253        process_watcher.virtual_memory = process_watcher.virtual_memory.max(virtual_memory);
254        process_watcher.run_time = process_watcher.run_time.max(run_time);
255        process_watcher.cpu_usage = process_watcher.cpu_usage.max(cpu_usage);
256
257        // Record the metrics to disk
258        self.record_metrics(process_watcher, true);
259
260        // Restart the measure timer
261        process_watcher.measure_timer.last_measure = Instant::now();
262    }
263
264    fn process_is_alive(&self, process_watcher: &ProcessWatcher) -> bool {
265        kill(process_watcher.pid_to_watch_nix, None).is_ok()
266    }
267
268    fn get_instant_now(&self) -> Instant {
269        Instant::now()
270    }
271
272    fn measure_timer_is_ready(&self, process_watcher: &ProcessWatcher) -> bool {
273        process_watcher.measure_timer.is_ready()
274    }
275
276    fn get_next_ready_timer<'a>(&self, timers: &[&'a Timer]) -> &'a Timer {
277        Timer::next_ready_timer(timers)
278    }
279
280    fn sleep(&self, duration: Duration) {
281        sleep(duration)
282    }
283
284    fn record_metrics(&self, process_watcher: &ProcessWatcher, running: bool) {
285        info_telemetry!(
286            exe_name = process_watcher.exe_name,
287            running,
288            run_time = process_watcher.run_time,
289            cpu_usage = f64::from(process_watcher.cpu_usage),
290            resident_memory = process_watcher.resident_memory,
291            virtual_memory = process_watcher.virtual_memory,
292        );
293    }
294
295    fn exit(&self, code: i32) -> ! {
296        exit(code)
297    }
298}
299
300#[derive(Default, Clone)]
301struct DefaultStartHelpers;
302impl StartHelpers for DefaultStartHelpers {}
303
304/// A helper struct to manage timers used in the `ProcessWatcher`
305struct Timer {
306    last_measure: Instant,
307    interval: Duration,
308}
309
310// A zero duration timer used as a default value
311static ZERO_TIMER: LazyLock<Timer> = LazyLock::new(|| Timer {
312    last_measure: Instant::now(),
313    interval: Duration::from_secs(0),
314});
315
316impl Timer {
317    fn new(interval: Duration) -> Self {
318        Self {
319            last_measure: Instant::now(),
320            interval,
321        }
322    }
323
324    /// Find the timer in the haystack that's going to go off next
325    fn next_ready_timer<'a>(timers: &[&'a Timer]) -> &'a Timer {
326        timers
327            .iter()
328            .min_by_key(|t| t.duration_until_ready())
329            .copied()
330            .unwrap_or(&ZERO_TIMER)
331    }
332
333    /// Check if the timer is ready to go off
334    fn is_ready(&self) -> bool {
335        Instant::now().duration_since(self.last_measure) >= self.interval
336    }
337
338    /// How long until the timer will go off
339    fn duration_until_ready(&self) -> Duration {
340        self.interval
341            .saturating_sub(Instant::now().duration_since(self.last_measure))
342    }
343}
344
345#[cfg(test)]
346mod config {
347    use super::*;
348    use crate::setup_fuelup_home;
349    use dirs::home_dir;
350    use rusty_fork::rusty_fork_test;
351    use std::env::set_var;
352
353    rusty_fork_test! {
354        #[test]
355        fn all_unset() {
356            let config = config().unwrap();
357
358            assert_eq!(
359                config.check_interval,
360                Duration::from_secs(PROCESSWATCHER_CHECK_INTERVAL.parse().unwrap())
361            );
362
363            assert_eq!(
364                config.measure_interval,
365                Duration::from_secs(PROCESSWATCHER_MEASURE_INTERVAL.parse().unwrap())
366            );
367
368            assert_eq!(
369                config.logfile,
370                PathBuf::from(
371                    &home_dir()
372                        .unwrap()
373                        .join(format!(".fuelup/log/{}.log", PROCESS_NAME))
374                )
375            );
376        }
377
378        #[test]
379        fn process_watcher_check_interval_invalid() {
380            set_var("PROCESSWATCHER_CHECK_INTERVAL", "invalid interval");
381
382            assert_eq!(
383                config().err(),
384                Some(TelemetryError::InvalidConfig(
385                    TelemetryError::Parse("invalid digit found in string".to_string()).into()
386                ))
387            );
388        }
389
390        #[test]
391        fn process_watcher_check_interval_set() {
392            set_var("PROCESSWATCHER_CHECK_INTERVAL", "2222");
393            assert_eq!(config().unwrap().check_interval, Duration::from_secs(2222));
394        }
395
396        #[test]
397        fn process_watcher_measure_interval_invalid() {
398            set_var("PROCESSWATCHER_MEASURE_INTERVAL", "invalid interval");
399            assert_eq!(
400                config().err(),
401                Some(TelemetryError::InvalidConfig(
402                    TelemetryError::Parse("invalid digit found in string".to_string()).into()
403                ))
404            );
405        }
406
407        #[test]
408        fn process_watcher_measure_interval_set() {
409            set_var("PROCESSWATCHER_MEASURE_INTERVAL", "2222");
410            assert_eq!(
411                config().unwrap().measure_interval,
412                Duration::from_secs(2222)
413            );
414        }
415
416        #[test]
417        fn all_set() {
418            setup_fuelup_home();
419            let fuelup_home = var("FUELUP_HOME").unwrap();
420
421            set_var("PROCESSWATCHER_CHECK_INTERVAL", "2222");
422            set_var("PROCESSWATCHER_MEASURE_INTERVAL", "3333");
423
424            let config = config().unwrap();
425
426            assert_eq!(config.check_interval, Duration::from_secs(2222));
427            assert_eq!(config.measure_interval, Duration::from_secs(3333));
428
429            assert_eq!(
430                config.logfile,
431                PathBuf::from(&fuelup_home).join(format!("log/{}.log", PROCESS_NAME))
432            );
433        }
434    }
435}
436
437#[cfg(test)]
438mod new {
439    use super::*;
440    use crate::setup_fuelup_home;
441    use rusty_fork::rusty_fork_test;
442
443    rusty_fork_test! {
444        #[test]
445        fn new() {
446            setup_fuelup_home();
447
448            let process_watcher = ProcessWatcher::new().unwrap();
449
450            assert_eq!(process_watcher.exe_name, get_process_name());
451
452            assert_eq!(process_watcher.pid_to_watch_nix, getpid());
453            assert_eq!(
454                process_watcher.pid_to_watch_sysinfo,
455                SysinfoPid::from(getpid().as_raw() as usize)
456            );
457
458            assert_eq!(process_watcher.check_timer.interval, Duration::from_secs(1));
459            assert_eq!(
460                process_watcher.measure_timer.interval,
461                Duration::from_secs(60)
462            );
463
464            assert_eq!(process_watcher.resident_memory, 0);
465            assert_eq!(process_watcher.virtual_memory, 0);
466            assert_eq!(process_watcher.run_time, 0);
467            assert_eq!(process_watcher.cpu_usage, 0.0);
468        }
469
470        #[test]
471        fn new_with_pid() {
472            setup_fuelup_home();
473
474            let process_watcher = ProcessWatcher::new_with_pid(NixPid::from_raw(1)).unwrap();
475
476            assert_eq!(process_watcher.exe_name, get_process_name());
477
478            assert_eq!(process_watcher.pid_to_watch_nix, NixPid::from_raw(1));
479            assert_eq!(process_watcher.pid_to_watch_sysinfo, SysinfoPid::from(1));
480
481            assert_eq!(process_watcher.check_timer.interval, Duration::from_secs(1));
482            assert_eq!(
483                process_watcher.measure_timer.interval,
484                Duration::from_secs(60)
485            );
486
487            assert_eq!(process_watcher.resident_memory, 0);
488            assert_eq!(process_watcher.virtual_memory, 0);
489            assert_eq!(process_watcher.run_time, 0);
490            assert_eq!(process_watcher.cpu_usage, 0.0);
491        }
492    }
493}
494
495// The following tests verify that the expected paths are taken on error. The
496// testing of the values themselves is handled in the modules after this one.
497
498#[cfg(test)]
499mod start {
500    use super::*;
501    use crate::{errors::into_fatal, setup_fuelup_home, WatcherError};
502    use nix::{
503        sys::wait::{waitpid, WaitStatus},
504        unistd::{dup2, fork, pipe, ForkResult},
505    };
506    use rusty_fork::rusty_fork_test;
507    use std::{
508        env::set_var,
509        fs::File,
510        io::{stdout, Read, Write},
511        os::fd::{AsRawFd, FromRawFd, IntoRawFd},
512        sync::Arc,
513    };
514
515    rusty_fork_test! {
516        #[test]
517        fn opted_out_is_true() {
518            setup_fuelup_home();
519
520            set_var("FUELUP_NO_TELEMETRY", "true");
521
522            let mut process_watcher = ProcessWatcher::new().unwrap();
523            let result = process_watcher.start();
524
525            assert_eq!(result, Ok(()));
526            assert!(!STARTED.load(Ordering::Relaxed));
527            assert_eq!(PID.load(Ordering::Relaxed), 0);
528
529            assert_eq!(process_watcher.resident_memory, 0);
530            assert_eq!(process_watcher.virtual_memory, 0);
531            assert_eq!(process_watcher.run_time, 0);
532            assert_eq!(process_watcher.cpu_usage, 0.0);
533        }
534
535        #[test]
536        fn opted_out_is_empty() {
537            setup_fuelup_home();
538
539            // Even though it's empty, we only care if it's set
540            set_var("FUELUP_NO_TELEMETRY", "");
541
542            let mut process_watcher = ProcessWatcher::new().unwrap();
543            let result = process_watcher.start();
544
545            assert_eq!(result, Ok(()));
546            assert!(!STARTED.load(Ordering::Relaxed));
547            assert_eq!(PID.load(Ordering::Relaxed), 0);
548
549            assert_eq!(process_watcher.resident_memory, 0);
550            assert_eq!(process_watcher.virtual_memory, 0);
551            assert_eq!(process_watcher.run_time, 0);
552            assert_eq!(process_watcher.cpu_usage, 0.0);
553        }
554
555        #[test]
556        fn already_started() {
557            setup_fuelup_home();
558
559            STARTED.store(true, Ordering::Relaxed);
560            PID.store(1, Ordering::Relaxed);
561
562            let mut process_watcher = ProcessWatcher::new().unwrap();
563            let result = process_watcher.start();
564
565            assert_eq!(result, Ok(()));
566            assert!(STARTED.load(Ordering::Relaxed));
567            assert_eq!(PID.load(Ordering::Relaxed), 1);
568
569            assert_eq!(process_watcher.resident_memory, 0);
570            assert_eq!(process_watcher.virtual_memory, 0);
571            assert_eq!(process_watcher.run_time, 0);
572            assert_eq!(process_watcher.cpu_usage, 0.0);
573
574            // Try to start it again to test re-entrance
575            let result = process_watcher.start();
576
577            assert_eq!(result, Ok(()));
578            assert!(STARTED.load(Ordering::Relaxed));
579            assert_eq!(PID.load(Ordering::Relaxed), 1);
580
581            assert_eq!(process_watcher.resident_memory, 0);
582            assert_eq!(process_watcher.virtual_memory, 0);
583            assert_eq!(process_watcher.run_time, 0);
584            assert_eq!(process_watcher.cpu_usage, 0.0);
585        }
586
587        #[test]
588        fn daemonise_failed() {
589            setup_fuelup_home();
590
591            struct DaemoniseFailed;
592
593            impl StartHelpers for DaemoniseFailed {
594                fn daemonise(&self, _logfile: &PathBuf) -> WatcherResult<Option<NixPid>> {
595                    Err(into_fatal(TelemetryError::Mock))
596                }
597            }
598
599            let mut process_watcher = ProcessWatcher::new().unwrap();
600            let result = process_watcher.start_with_helpers(&DaemoniseFailed);
601
602            assert_eq!(
603                result.err(),
604                Some(WatcherError::Fatal(TelemetryError::Mock))
605            );
606            assert!(!STARTED.load(Ordering::Relaxed));
607            assert_eq!(PID.load(Ordering::Relaxed), 0);
608        }
609
610        #[test]
611        fn daemonise_is_parent() {
612            setup_fuelup_home();
613
614            struct DaemoniseIsParent;
615
616            impl StartHelpers for DaemoniseIsParent {
617                fn daemonise(&self, _logfile: &PathBuf) -> WatcherResult<Option<NixPid>> {
618                    Ok(Some(NixPid::from_raw(1337)))
619                }
620            }
621
622            let mut process_watcher = ProcessWatcher::new().unwrap();
623            let result = process_watcher.start_with_helpers(&DaemoniseIsParent);
624
625            assert_eq!(result, Ok(()));
626            assert!(STARTED.load(Ordering::Relaxed));
627            assert_eq!(PID.load(Ordering::Relaxed), 1337);
628        }
629
630        #[test]
631        fn new_fuel_telemetry_failed() {
632            setup_fuelup_home();
633
634            struct NewFuelTelemetryFailed;
635
636            impl StartHelpers for NewFuelTelemetryFailed {
637                fn new_fuel_telemetry(
638                    &self,
639                ) -> Result<(
640                    tracing_subscriber::filter::Filtered<
641                        tracing_subscriber::fmt::Layer<
642                            tracing_subscriber::Registry,
643                            tracing_subscriber::fmt::format::DefaultFields,
644                            telemetry_formatter::TelemetryFormatter,
645                            tracing_appender::non_blocking::NonBlocking,
646                        >,
647                        tracing_subscriber::EnvFilter,
648                        tracing_subscriber::Registry,
649                    >,
650                    tracing_appender::non_blocking::WorkerGuard,
651                )> {
652                    Err(TelemetryError::Mock)
653                }
654
655                fn daemonise(&self, _logfile: &PathBuf) -> WatcherResult<Option<NixPid>> {
656                    Ok(None)
657                }
658            }
659
660            let mut process_watcher = ProcessWatcher::new().unwrap();
661            let result = process_watcher.start_with_helpers(&NewFuelTelemetryFailed);
662
663            assert_eq!(
664                result.err(),
665                Some(WatcherError::Fatal(TelemetryError::Mock))
666            );
667        }
668
669        #[test]
670        fn process_is_dead() {
671            setup_fuelup_home();
672
673            #[derive(Default, Clone)]
674            struct ProcessIsDead;
675
676            impl StartHelpers for ProcessIsDead {
677                fn process_is_alive(&self, _process_watcher: &ProcessWatcher) -> bool {
678                    false
679                }
680
681                fn daemonise(&self, _logfile: &PathBuf) -> WatcherResult<Option<NixPid>> {
682                    Ok(None)
683                }
684
685                fn exit(&self, _code: i32) -> ! {
686                    // Test we actually exited via our expected code path
687                    exit(99);
688                }
689            }
690
691            match unsafe { fork() }.unwrap() {
692                ForkResult::Parent { child } => match waitpid(child, None).unwrap() {
693                    WaitStatus::Exited(_, code) => {
694                        assert_eq!(code, 99);
695                    }
696                    _ => panic!("Child did not exit normally"),
697                },
698                ForkResult::Child => {
699                    let mut process_watcher = ProcessWatcher::new().unwrap();
700                    let _ = process_watcher.start_with_helpers(&ProcessIsDead);
701
702                    // Incorrect status code that rusty_fork_test!() will detect
703                    exit(86);
704                }
705            }
706        }
707
708        #[test]
709        fn measure_process_called_when_ready() {
710            setup_fuelup_home();
711
712            #[derive(Default, Clone)]
713            struct MeasureProcessCalledWhenReady {
714                default_helpers: DefaultStartHelpers,
715                measure_process_called_count: Arc<AtomicI32>,
716            }
717
718            impl StartHelpers for MeasureProcessCalledWhenReady {
719                fn measure_process(&self, process_watcher: &mut ProcessWatcher) {
720                    self.measure_process_called_count
721                        .fetch_add(1, Ordering::Relaxed);
722
723                    stdout()
724                        .write_all(
725                            format!(
726                                "measure_process_called_count: {}",
727                                self.measure_process_called_count.load(Ordering::Relaxed)
728                            )
729                            .as_bytes(),
730                        )
731                        .unwrap();
732                    stdout().flush().unwrap();
733
734                    self.default_helpers.measure_process(process_watcher);
735                }
736
737                fn process_is_alive(&self, _process_watcher: &ProcessWatcher) -> bool {
738                    stdout()
739                        .write_all("process_is_alive called".as_bytes())
740                        .unwrap();
741                    stdout().flush().unwrap();
742
743                    self.measure_process_called_count.load(Ordering::Relaxed) < 2
744                }
745
746                fn get_instant_now(&self) -> Instant {
747                    stdout()
748                        .write_all("get_instant_now called".as_bytes())
749                        .unwrap();
750                    stdout().flush().unwrap();
751
752                    Instant::now()
753                }
754
755                fn measure_timer_is_ready(&self, _process_watcher: &ProcessWatcher) -> bool {
756                    stdout()
757                        .write_all("measure_timer_is_ready called".as_bytes())
758                        .unwrap();
759                    stdout().flush().unwrap();
760
761                    true
762                }
763
764                fn get_next_ready_timer<'a>(&self, timers: &[&'a Timer]) -> &'a Timer {
765                    stdout()
766                        .write_all("get_next_ready_timer called".as_bytes())
767                        .unwrap();
768                    stdout().flush().unwrap();
769
770                    timers[0]
771                }
772
773                fn sleep(&self, _duration: Duration) {
774                    stdout().write_all("sleep called".as_bytes()).unwrap();
775                    stdout().flush().unwrap();
776                }
777
778                fn daemonise(&self, _logfile: &PathBuf) -> WatcherResult<Option<NixPid>> {
779                    Ok(None)
780                }
781
782                fn record_metrics(&self, _process_watcher: &ProcessWatcher, _running: bool) {
783                    stdout()
784                        .write_all("record_metrics called".as_bytes())
785                        .unwrap();
786                    stdout().flush().unwrap();
787                }
788
789                fn exit(&self, _code: i32) -> ! {
790                    stdout().write_all("exit called".as_bytes()).unwrap();
791                    stdout().flush().unwrap();
792
793                    // Test we actually exited via our expected code path
794                    exit(99);
795                }
796            }
797
798            let (read_fd, write_fd) = pipe().unwrap();
799            let mut pipe_read = unsafe { File::from_raw_fd(read_fd.into_raw_fd()) };
800            let pipe_write = unsafe { File::from_raw_fd(write_fd.into_raw_fd()) };
801
802            match unsafe { fork() }.unwrap() {
803                ForkResult::Parent { child } => {
804                    drop(pipe_write);
805
806                    let mut output = String::new();
807                    pipe_read.read_to_string(&mut output).unwrap();
808
809                    assert!(output.contains("measure_process_called_count: 2"));
810                    assert!(output.contains("process_is_alive called"));
811                    assert!(output.contains("get_instant_now called"));
812                    assert!(output.contains("measure_timer_is_ready called"));
813                    assert!(output.contains("get_next_ready_timer called"));
814                    assert!(output.contains("sleep called"));
815                    assert!(output.contains("record_metrics called"));
816                    assert!(output.contains("exit called"));
817
818                    match waitpid(child, None).unwrap() {
819                        WaitStatus::Exited(_, code) => {
820                            assert_eq!(code, 99);
821                        }
822                        _ => panic!("Child did not exit normally"),
823                    }
824                }
825                ForkResult::Child => {
826                    drop(pipe_read);
827                    dup2(pipe_write.as_raw_fd(), 1).unwrap();
828
829                    let start_helpers = MeasureProcessCalledWhenReady::default();
830                    let mut process_watcher = ProcessWatcher::new().unwrap();
831                    let _ = process_watcher.start_with_helpers(&start_helpers);
832
833                    // Incorrect status code that rusty_fork_test!() will detect
834                    exit(86);
835                }
836            }
837        }
838    }
839}
840
841#[cfg(test)]
842mod measure_process {
843    use super::*;
844    use crate::setup_fuelup_home;
845    use nix::{
846        sys::{
847            signal::{kill, Signal},
848            wait::{waitpid, WaitPidFlag, WaitStatus},
849        },
850        unistd::{fork, ForkResult},
851    };
852    use rusty_fork::rusty_fork_test;
853    use std::sync::Arc;
854
855    rusty_fork_test! {
856        #[test]
857        fn process_died() {
858            setup_fuelup_home();
859
860            struct ProcessDied;
861            impl StartHelpers for ProcessDied {}
862
863            let mut kill_called = false;
864
865            match unsafe { fork() }.unwrap() {
866                ForkResult::Parent { child } => {
867                    let helpers = ProcessDied;
868                    let mut process_watcher =
869                        ProcessWatcher::new_with_pid(NixPid::from_raw(child.as_raw())).unwrap();
870
871                    assert_eq!(process_watcher.resident_memory, 0);
872                    assert_eq!(process_watcher.virtual_memory, 0);
873                    assert_eq!(process_watcher.run_time, 0);
874                    assert_eq!(process_watcher.cpu_usage, 0.0);
875
876                    loop {
877                        match waitpid(child, Some(WaitPidFlag::WNOHANG)) {
878                            Ok(WaitStatus::StillAlive) => {
879                                if !kill_called {
880                                    kill(child, Signal::SIGKILL).unwrap();
881                                    kill_called = true;
882                                }
883                            }
884                            Ok(WaitStatus::Signaled(child_pid, signal, _)) => {
885                                assert_eq!(child_pid, child);
886                                assert_eq!(signal, Signal::SIGKILL);
887
888                                helpers.measure_process(&mut process_watcher);
889
890                                assert_eq!(process_watcher.resident_memory, 0);
891                                assert_eq!(process_watcher.virtual_memory, 0);
892                                assert_eq!(process_watcher.run_time, 0);
893                                assert_eq!(process_watcher.cpu_usage, 0.0);
894
895                                break;
896                            }
897                            _ => panic!("Child process terminated unexpectedly"),
898                        }
899                    }
900                }
901                ForkResult::Child => {
902                    loop {
903                        // Sleep until we're killed off
904                        sleep(Duration::from_secs(1));
905                    }
906                }
907            }
908        }
909
910        #[test]
911        fn ok() {
912            setup_fuelup_home();
913
914            #[derive(Default, Clone)]
915            struct AOk {
916                record_metrics_called: Arc<AtomicBool>,
917            }
918
919            impl StartHelpers for AOk {
920                fn record_metrics(&self, _process_watcher: &ProcessWatcher, _running: bool) {
921                    self.record_metrics_called.store(true, Ordering::Relaxed);
922                }
923            }
924
925            let mut process_watcher = ProcessWatcher::new().unwrap();
926
927            assert_eq!(process_watcher.resident_memory, 0);
928            assert_eq!(process_watcher.virtual_memory, 0);
929            assert_eq!(process_watcher.run_time, 0);
930            assert_eq!(process_watcher.cpu_usage, 0.0);
931
932            let helpers = AOk::default();
933            helpers.measure_process(&mut process_watcher);
934
935            let at_least_one_metric_is_set = process_watcher.resident_memory > 0
936                || process_watcher.virtual_memory > 0
937                || process_watcher.run_time > 0
938                || process_watcher.cpu_usage > 0.0;
939
940            assert!(at_least_one_metric_is_set);
941
942            assert!(helpers.record_metrics_called.load(Ordering::Relaxed));
943        }
944    }
945}
946
947#[cfg(test)]
948mod process_is_alive {
949    use super::*;
950    use crate::setup_fuelup_home;
951    use nix::{
952        sys::{
953            signal::{kill, Signal},
954            wait::{waitpid, WaitPidFlag, WaitStatus},
955        },
956        unistd::{fork, ForkResult},
957    };
958    use rusty_fork::rusty_fork_test;
959
960    rusty_fork_test! {
961        #[test]
962        fn ok() {
963            setup_fuelup_home();
964
965            struct AOk;
966            impl StartHelpers for AOk {}
967
968            let mut kill_called = false;
969
970            match unsafe { fork() }.unwrap() {
971                ForkResult::Parent { child } => {
972                    let helpers = AOk;
973                    let process_watcher =
974                        ProcessWatcher::new_with_pid(NixPid::from_raw(child.as_raw())).unwrap();
975
976                    assert!(helpers.process_is_alive(&process_watcher));
977
978                    loop {
979                        match waitpid(child, Some(WaitPidFlag::WNOHANG)) {
980                            Ok(WaitStatus::StillAlive) => {
981                                if !kill_called {
982                                    kill(child, Signal::SIGKILL).unwrap();
983                                    kill_called = true;
984                                }
985                            }
986                            Ok(WaitStatus::Signaled(child_pid, signal, _)) => {
987                                assert_eq!(child_pid, child);
988                                assert_eq!(signal, Signal::SIGKILL);
989                                assert!(!helpers.process_is_alive(&process_watcher));
990                                break;
991                            }
992                            _ => panic!("Child process terminated unexpectedly"),
993                        }
994                    }
995                }
996                ForkResult::Child => {
997                    loop {
998                        // Sleep until we're killed off
999                        sleep(Duration::from_secs(1));
1000                    }
1001                }
1002            }
1003        }
1004    }
1005}
1006
1007#[cfg(test)]
1008mod measure_timer_is_ready {
1009    use super::*;
1010    use crate::setup_fuelup_home;
1011    use rusty_fork::rusty_fork_test;
1012
1013    rusty_fork_test! {
1014        #[test]
1015        fn not_ready() {
1016            setup_fuelup_home();
1017
1018            struct NotReady;
1019            impl StartHelpers for NotReady {}
1020
1021            let mut process_watcher = ProcessWatcher::new().unwrap();
1022
1023            let measure_interval = process_watcher.measure_timer.interval;
1024            process_watcher.measure_timer.last_measure = Instant::now() + (measure_interval * 2);
1025
1026            let helpers = NotReady;
1027            assert!(!helpers.measure_timer_is_ready(&process_watcher));
1028        }
1029
1030        #[test]
1031        fn equal() {
1032            setup_fuelup_home();
1033
1034            struct Equal;
1035            impl StartHelpers for Equal {}
1036
1037            let mut process_watcher = ProcessWatcher::new().unwrap();
1038
1039            let measure_interval = process_watcher.measure_timer.interval;
1040            process_watcher.measure_timer.last_measure = Instant::now() - measure_interval;
1041
1042            let helpers = Equal;
1043            assert!(helpers.measure_timer_is_ready(&process_watcher));
1044        }
1045
1046        #[test]
1047        fn late() {
1048            setup_fuelup_home();
1049
1050            struct Late;
1051            impl StartHelpers for Late {}
1052
1053            let mut process_watcher = ProcessWatcher::new().unwrap();
1054
1055            let measure_interval = process_watcher.measure_timer.interval;
1056            process_watcher.measure_timer.last_measure = Instant::now() - (measure_interval * 2);
1057
1058            let helpers = Late;
1059            assert!(helpers.measure_timer_is_ready(&process_watcher));
1060        }
1061    }
1062}
1063
1064#[cfg(test)]
1065mod get_next_ready_timer {
1066    use super::*;
1067    use crate::setup_fuelup_home;
1068    use rusty_fork::rusty_fork_test;
1069
1070    rusty_fork_test! {
1071        #[test]
1072        fn no_timers() {
1073            setup_fuelup_home();
1074
1075            struct NoTimers;
1076            impl StartHelpers for NoTimers {}
1077
1078            let helpers = NoTimers;
1079            let results = helpers.get_next_ready_timer(&[]);
1080
1081            assert_eq!(results.interval, ZERO_TIMER.interval);
1082            assert_eq!(results.last_measure, ZERO_TIMER.last_measure);
1083        }
1084
1085        #[test]
1086        fn one_timer() {
1087            setup_fuelup_home();
1088
1089            struct OneTimer;
1090            impl StartHelpers for OneTimer {}
1091
1092            let helpers = OneTimer;
1093            let timer = Timer::new(Duration::from_secs(1));
1094            let results = helpers.get_next_ready_timer(&[&timer]);
1095
1096            assert_eq!(results.interval, timer.interval);
1097            assert_eq!(results.last_measure, timer.last_measure);
1098        }
1099
1100        #[test]
1101        fn two_timers() {
1102            setup_fuelup_home();
1103
1104            struct TwoTimers;
1105            impl StartHelpers for TwoTimers {}
1106
1107            let helpers = TwoTimers;
1108            let timer1 = Timer::new(Duration::from_secs(10));
1109            let timer2 = Timer::new(Duration::from_secs(20));
1110
1111            let results = helpers.get_next_ready_timer(&[&timer1, &timer2]);
1112            assert_eq!(results.interval, timer1.interval);
1113            assert_eq!(results.last_measure, timer1.last_measure);
1114
1115            let results = helpers.get_next_ready_timer(&[&timer2, &timer1]);
1116            assert_eq!(results.interval, timer1.interval);
1117            assert_eq!(results.last_measure, timer1.last_measure);
1118        }
1119    }
1120}
1121
1122#[cfg(test)]
1123mod timer_new {
1124    use super::*;
1125    use rusty_fork::rusty_fork_test;
1126
1127    rusty_fork_test! {
1128        #[test]
1129        fn zero_duration() {
1130            let timer = Timer::new(Duration::from_secs(0));
1131            assert_eq!(timer.interval, Duration::from_secs(0));
1132            assert!(timer.last_measure <= Instant::now());
1133        }
1134
1135        #[test]
1136        fn new() {
1137            let timer = Timer::new(Duration::from_secs(123));
1138            assert_eq!(timer.interval, Duration::from_secs(123));
1139            assert!(timer.last_measure <= Instant::now());
1140        }
1141    }
1142}
1143
1144#[cfg(test)]
1145mod next_ready_timer {
1146    use super::*;
1147    use rusty_fork::rusty_fork_test;
1148
1149    rusty_fork_test! {
1150        #[test]
1151        fn no_timers() {
1152            let result = Timer::next_ready_timer(&[]);
1153            assert_eq!(result.interval, ZERO_TIMER.interval);
1154            assert_eq!(result.last_measure, ZERO_TIMER.last_measure);
1155        }
1156
1157        #[test]
1158        fn one_timer() {
1159            let timer = Timer::new(Duration::from_secs(123));
1160            let result = Timer::next_ready_timer(&[&timer]);
1161            assert_eq!(result.interval, timer.interval);
1162            assert_eq!(result.last_measure, timer.last_measure);
1163        }
1164
1165        #[test]
1166        fn two_timers() {
1167            let timer1 = Timer::new(Duration::from_secs(10));
1168            let timer2 = Timer::new(Duration::from_secs(20));
1169
1170            let result = Timer::next_ready_timer(&[&timer1, &timer2]);
1171            assert_eq!(result.interval, timer1.interval);
1172            assert_eq!(result.last_measure, timer1.last_measure);
1173
1174            let result = Timer::next_ready_timer(&[&timer2, &timer1]);
1175            assert_eq!(result.interval, timer1.interval);
1176            assert_eq!(result.last_measure, timer1.last_measure);
1177        }
1178    }
1179}
1180
1181#[cfg(test)]
1182mod is_ready {
1183    use super::*;
1184    use rusty_fork::rusty_fork_test;
1185
1186    rusty_fork_test! {
1187        #[test]
1188        fn not_ready() {
1189            let mut timer = Timer::new(Duration::from_secs(10));
1190
1191            let measure_interval = timer.interval;
1192            timer.last_measure = Instant::now() + (measure_interval * 2);
1193
1194            assert!(!timer.is_ready());
1195        }
1196
1197        #[test]
1198        fn equal() {
1199            let mut timer = Timer::new(Duration::from_secs(10));
1200
1201            let measure_interval = timer.interval;
1202            timer.last_measure = Instant::now() - measure_interval;
1203
1204            assert!(timer.is_ready());
1205        }
1206
1207        #[test]
1208        fn late() {
1209            let mut timer = Timer::new(Duration::from_secs(10));
1210
1211            let measure_interval = timer.interval;
1212            timer.last_measure = Instant::now() - (measure_interval * 2);
1213
1214            assert!(timer.is_ready());
1215        }
1216    }
1217}
1218
1219#[cfg(test)]
1220mod duration_until_ready {
1221    use super::*;
1222    use rusty_fork::rusty_fork_test;
1223
1224    rusty_fork_test! {
1225        #[test]
1226        fn not_ready() {
1227            let mut timer = Timer::new(Duration::from_secs(10));
1228
1229            let measure_interval = timer.interval;
1230            timer.last_measure = Instant::now() + (measure_interval * 2);
1231
1232            assert_eq!(timer.duration_until_ready(), measure_interval);
1233        }
1234
1235        #[test]
1236        fn equal() {
1237            let mut timer = Timer::new(Duration::from_secs(10));
1238
1239            let measure_interval = timer.interval;
1240            timer.last_measure = Instant::now() - measure_interval;
1241
1242            assert_eq!(timer.duration_until_ready(), Duration::from_secs(0));
1243        }
1244
1245        #[test]
1246        fn late() {
1247            let mut timer = Timer::new(Duration::from_secs(10));
1248
1249            let measure_interval = timer.interval;
1250            timer.last_measure = Instant::now() - (measure_interval * 2);
1251
1252            assert_eq!(timer.duration_until_ready(), Duration::from_secs(0));
1253        }
1254    }
1255}