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
27const 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 logfile: PathBuf,
39 check_interval: Duration,
41 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 exe_name: String,
78
79 pid_to_watch_nix: NixPid,
81 pid_to_watch_sysinfo: SysinfoPid,
83
84 check_timer: Timer,
86 measure_timer: Timer,
88
89 resident_memory: u64,
91 virtual_memory: u64,
92 run_time: u64,
93 cpu_usage: f32,
94}
95
96static STARTED: AtomicBool = AtomicBool::new(false);
98
99pub 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 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 PID.store(pid.as_raw(), Ordering::Relaxed);
146 return Ok(());
147 }
148 Err(e) => {
149 STARTED.store(false, Ordering::Relaxed);
151 PID.store(0, Ordering::Relaxed);
152 return Err(e);
153 }
154 Ok(None) => {
155 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 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 helpers.record_metrics(self, false);
182
183 helpers.exit(0);
184 }
185
186 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 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 return;
244 };
245
246 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 self.record_metrics(process_watcher, true);
259
260 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
304struct Timer {
306 last_measure: Instant,
307 interval: Duration,
308}
309
310static 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 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 fn is_ready(&self) -> bool {
335 Instant::now().duration_since(self.last_measure) >= self.interval
336 }
337
338 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#[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 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 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 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 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 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 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(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(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}