Skip to main content

fuel_telemetry/
lib.rs

1pub mod errors;
2pub mod file_watcher;
3pub mod process_watcher;
4pub mod systeminfo_watcher;
5pub mod telemetry_formatter;
6pub mod telemetry_layer;
7
8pub use errors::{into_fatal, into_recoverable, TelemetryError, WatcherError};
9pub use fuel_telemetry_macros::{new, new_with_watchers, new_with_watchers_and_init};
10pub use telemetry_formatter::TelemetryFormatter;
11pub use telemetry_layer::TelemetryLayer;
12pub use tracing::{debug, error, event, info, span, trace, warn, Level};
13pub use tracing_appender::non_blocking::WorkerGuard;
14
15pub mod prelude {
16    pub use crate::{
17        debug, debug_telemetry, error, error_telemetry, event, info, info_telemetry, span,
18        span_telemetry, trace, trace_telemetry, warn, warn_telemetry, Level, TelemetryLayer,
19    };
20}
21
22// Re-export tracing so proc_macros can use them
23pub use tracing as __reexport_tracing;
24pub use tracing_subscriber as __reexport_tracing_subscriber;
25pub use tracing_subscriber::filter::EnvFilter as __reexport_EnvFilter;
26pub use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt as __reexport_tracing_subscriber_SubscriberExt;
27pub use tracing_subscriber::util::SubscriberInitExt as __reexport_SubscriberInitExt;
28pub use tracing_subscriber::Layer as __reexport_Layer;
29
30use dirs::home_dir;
31use libc::{c_int, c_long};
32use nix::{
33    errno::Errno,
34    fcntl::{Flock, FlockArg},
35    sys::stat,
36    unistd::{
37        chdir, close, dup2, fork, getpid, pipe, read, setsid, sysconf, write, ForkResult, Pid,
38        SysconfVar,
39    },
40};
41use std::{
42    env::{current_exe, var, var_os},
43    fs::{create_dir_all, File, OpenOptions},
44    io::{stderr, stdout, Write},
45    os::fd::{AsRawFd, OwnedFd},
46    path::{Path, PathBuf},
47    process::exit,
48    sync::LazyLock,
49};
50
51// We need to close all file descriptors in `daemonise()`, but need to skip the
52// first three (0 = STDIN, 1 = STDOUT, 2 = STDERR) as we deal with stdio later
53const FIRST_NON_STDIO_FD: i32 = 3;
54
55// The lowest maximum file descriptor across Legacy Linux and MacOS
56const MIN_OPEN_MAX: i32 = 1024;
57
58// Result type for the crate
59pub type Result<T> = std::result::Result<T, TelemetryError>;
60
61// Result type from Watchers
62pub type WatcherResult<T> = std::result::Result<T, WatcherError>;
63
64//
65// Crate static configuration
66//
67
68/// A helper struct to get environment variables with a default value
69pub struct EnvSetting {
70    /// The name of the environment variable
71    name: &'static str,
72    /// The default value of the environment variable
73    default: &'static str,
74}
75
76impl EnvSetting {
77    /// Creates a new `EnvSetting`
78    ///
79    /// This function creates a new `EnvSetting` with the given name and default value.
80    ///
81    /// ```rust
82    /// use fuel_telemetry::EnvSetting;
83    ///
84    /// let env_setting = EnvSetting::new("FUELUP_HOME", ".fuelup");
85    /// ```
86    pub fn new(name: &'static str, default: &'static str) -> Self {
87        Self { name, default }
88    }
89
90    /// Gets the environment variable with a default value
91    ///
92    /// This function gets the environment variable with a default value.
93    ///
94    /// ```rust
95    /// use fuel_telemetry::EnvSetting;
96    ///
97    /// let env_setting = EnvSetting::new("FUELUP_HOME", ".fuelup");
98    /// let fuelup_home = env_setting.get();
99    /// ```
100    pub fn get(&self) -> String {
101        var(self.name).unwrap_or_else(|_| self.default.to_string())
102    }
103}
104
105/// Telemetry's global configuration
106///
107/// This struct contains the configuration for telemetry, to be used here and
108/// within its underlying modules.
109pub struct TelemetryConfig {
110    // The path to the fuelup tmp directory
111    fuelup_tmp: String,
112    // The path to the fuelup log directory
113    fuelup_log: String,
114}
115
116/// Get the global telemetry configuration
117///
118/// This function returns the global `'static` telemetry configuration.
119///
120/// ```rust
121/// use fuel_telemetry::telemetry_config;
122///
123/// let telemetry_config = telemetry_config();
124/// ```
125pub fn telemetry_config() -> Result<&'static TelemetryConfig> {
126    // Note: because we are using LazyLock, we cannot mock this function
127    // using helpers as they cannot be evaluated as non-const.
128    pub static TELEMETRY_CONFIG: LazyLock<Result<TelemetryConfig>> = LazyLock::new(|| {
129        let fuelup_home_env = EnvSetting {
130            name: "FUELUP_HOME",
131            default: ".fuelup",
132        };
133
134        let fuelup_tmp_env = EnvSetting {
135            name: "FUELUP_TMP",
136            default: "tmp",
137        };
138
139        let fuelup_log_env = EnvSetting {
140            name: "FUELUP_LOG",
141            default: "log",
142        };
143
144        // Tries to set the fuelup home directory from the environment, falling
145        // back to the $HOME/.fuelup
146        let fuelup_home = var_os(fuelup_home_env.name)
147            .map(PathBuf::from)
148            .or_else(|| home_dir().map(|dir| dir.join(fuelup_home_env.default)))
149            .ok_or(TelemetryError::UnreachableHomeDir)?
150            .into_os_string()
151            .into_string()
152            .map_err(|e| TelemetryError::InvalidHomeDir(e.to_string_lossy().into()))?;
153
154        // Tries to set the fuelup tmp directory from the environment, falling
155        // back to $FUELUP_HOME/tmp
156        let fuelup_tmp = var_os(fuelup_tmp_env.name)
157            .unwrap_or_else(|| {
158                PathBuf::from(fuelup_home.clone())
159                    .join(fuelup_tmp_env.default)
160                    .into_os_string()
161            })
162            .into_string()
163            .map_err(|e| TelemetryError::InvalidTmpDir(e.to_string_lossy().into()))?;
164
165        // Tries to set the fuelup log directory from the environment, falling
166        // back to $FUELUP_HOME/log
167        let fuelup_log = var_os(fuelup_log_env.name)
168            .unwrap_or_else(|| {
169                PathBuf::from(fuelup_home.clone())
170                    .join(fuelup_log_env.default)
171                    .into_os_string()
172            })
173            .into_string()
174            .map_err(|e| TelemetryError::InvalidLogDir(e.to_string_lossy().into()))?;
175
176        // Create the fuelup tmp and log directories if they don't exist
177        create_dir_all(&fuelup_tmp)?;
178        create_dir_all(&fuelup_log)?;
179
180        Ok(TelemetryConfig {
181            fuelup_tmp,
182            fuelup_log,
183        })
184    });
185
186    TELEMETRY_CONFIG
187        .as_ref()
188        .map_err(|e| TelemetryError::InvalidConfig(e.to_string()))
189}
190
191//
192// Convenience Macros
193//
194
195/// Enter a temporary `Span`, then generates an `Event` with telemetry enabled
196///
197/// Note: The `Span` name is currently hardcoded to "auto" as `tracing::span!`
198/// requires the name to be `const` as internally it is evaluated as a static,
199/// however getting the caller's function name in statics is experimental.
200///
201/// ```rust
202/// use fuel_telemetry::prelude::*;
203///
204/// span_telemetry!(Level::INFO, "This event will be sent to InfluxDB");
205/// ```
206#[macro_export]
207macro_rules! span_telemetry {
208    ($level:expr, $($arg:tt)*) => {
209        $crate::__reexport_tracing::span!($level, "auto", telemetry = true).in_scope(|| {
210            $crate::__reexport_tracing::event!($level, $($arg)*)
211        })
212    }
213}
214
215/// Generate an `ERROR` telemetry `Event`
216///
217/// ```rust
218/// use fuel_telemetry::prelude::*;
219///
220/// error_telemetry!("This error event will be sent to InfluxDB");
221/// ```
222#[macro_export]
223macro_rules! error_telemetry {
224    ($($arg:tt)*) => {{
225        span_telemetry!($crate::__reexport_tracing::Level::ERROR, $($arg)*);
226    }}
227}
228
229/// Generate a `WARN` telemetry `Event`
230///
231/// ```rust
232/// use fuel_telemetry::prelude::*;
233///
234/// warn_telemetry!("This warn event will be sent to InfluxDB");
235/// ```
236#[macro_export]
237macro_rules! warn_telemetry {
238    ($($arg:tt)*) => {{
239        span_telemetry!($crate::__reexport_tracing::Level::WARN, $($arg)*);
240    }}
241}
242
243/// Generate an `INFO` telemetry `Event`
244///
245/// ```rust
246/// use fuel_telemetry::prelude::*;
247///
248/// info_telemetry!("This info event will be sent to InfluxDB");
249/// ```
250#[macro_export]
251macro_rules! info_telemetry {
252    ($($arg:tt)*) => {{
253        span_telemetry!($crate::__reexport_tracing::Level::INFO, $($arg)*);
254    }}
255}
256
257/// Generate a `DEBUG` telemetry `Event`
258///
259/// ```rust
260/// use fuel_telemetry::prelude::*;
261///
262/// debug_telemetry!("This debug event will be sent to InfluxDB");
263/// ```
264#[macro_export]
265macro_rules! debug_telemetry {
266    ($($arg:tt)*) => {{
267        span_telemetry!($crate::__reexport_tracing::Level::DEBUG, $($arg)*);
268    }}
269}
270
271/// Generate a `TRACE` telemetry `Event`
272///
273/// ```rust
274/// use fuel_telemetry::prelude::*;
275///
276/// trace_telemetry!("This trace event will be sent to InfluxDB");
277/// ```
278#[macro_export]
279macro_rules! trace_telemetry {
280    ($($arg:tt)*) => {{
281        span_telemetry!($crate::__reexport_tracing::Level::TRACE, $($arg)*);
282    }}
283}
284
285/// Helper function to get the current process' binary filename
286pub fn get_process_name() -> String {
287    let mut exe_name = String::from("unknown");
288
289    if let Ok(exe) = current_exe() {
290        if let Some(name) = exe.file_name() {
291            if let Some(name_str) = name.to_str() {
292                exe_name = name_str.to_string().replace(':', "_");
293            }
294        }
295    }
296
297    exe_name
298}
299
300/// Enforce a singleton by taking an advisory lock on a file
301///
302/// This function takes an advisory lock on a file, and if another process has
303/// already locked the file, it will exit the current process.
304pub(crate) fn enforce_singleton(filename: &Path) -> Result<Flock<File>> {
305    enforce_singleton_with_helpers(filename, &mut DefaultEnforceSingletonHelpers)
306}
307
308fn enforce_singleton_with_helpers(
309    filename: &Path,
310    helpers: &mut impl EnforceSingletonHelpers,
311) -> Result<Flock<File>> {
312    let lockfile = helpers.open(filename)?;
313
314    let lock = match helpers.lock(lockfile) {
315        Ok(lock) => lock,
316        Err((_, Errno::EWOULDBLOCK)) => {
317            // Silently exit as another process has already locked the file
318            helpers.exit(0);
319        }
320        Err((_, e)) => return Err(TelemetryError::from(e)),
321    };
322
323    Ok(lock)
324}
325
326trait EnforceSingletonHelpers {
327    fn open(&self, filename: &Path) -> std::result::Result<File, std::io::Error> {
328        OpenOptions::new().create(true).append(true).open(filename)
329    }
330
331    fn lock(&self, file: File) -> std::result::Result<Flock<File>, (File, Errno)> {
332        Flock::lock(file, FlockArg::LockExclusiveNonblock)
333    }
334
335    fn exit(&self, status: i32) -> ! {
336        exit(status)
337    }
338}
339
340struct DefaultEnforceSingletonHelpers;
341impl EnforceSingletonHelpers for DefaultEnforceSingletonHelpers {}
342
343/// Daemonise the current process
344///
345/// This function forks and has the parent immediately return. The forked off
346/// child then follows the common "double-fork" method of daemonising.
347pub(crate) fn daemonise(log_filename: &PathBuf) -> WatcherResult<Option<Pid>> {
348    let mut helpers = DefaultDaemoniseHelpers;
349    daemonise_with_helpers(log_filename, &mut helpers)
350}
351
352fn daemonise_with_helpers(
353    log_filename: &PathBuf,
354    helpers: &mut impl DaemoniseHelpers,
355) -> WatcherResult<Option<Pid>> {
356    // All errors before the first fork() are recoverable from the caller,
357    // meaning that the error occured within the same process and should be
358    // ignored by the caller so that it can continue
359
360    helpers.flush(&mut stdout()).map_err(into_recoverable)?;
361    helpers.flush(&mut stderr()).map_err(into_recoverable)?;
362
363    let (read_fd, write_fd) = helpers.pipe().map_err(into_recoverable)?;
364
365    // Return if we are the parent
366    if helpers.fork().map_err(into_recoverable)?.is_parent() {
367        drop(write_fd);
368
369        let mut pid_bytes = [0u8; std::mem::size_of::<Pid>()];
370        helpers
371            .read_pipe(read_fd, &mut pid_bytes)
372            .map_err(into_recoverable)?;
373
374        return Ok(Some(Pid::from_raw(i32::from_ne_bytes(pid_bytes))));
375    };
376
377    drop(read_fd);
378
379    // From here on, we are no longer the original process, so the caller should
380    // treat errors as fatal. This means that on error the process should exit
381    // immediately as there should not be two identical flows of execution
382
383    // To prevent us from becoming a zombie when we die, we fork then kill the
384    // parent so that we are immediately inherited by init/systemd. Doing so, we
385    // are guaranteed to be reaped on exit.
386    //
387    // Also, doing this guarantees that we are not the group leader, which is
388    // required to create a new session (i.e setsid() will fail otherwise)
389    if helpers.fork().map_err(into_fatal)?.is_parent() {
390        drop(write_fd);
391        exit(0);
392    }
393
394    // Creating a new session means we won't receive signals to the original
395    // group or session (e.g. hitting CTRL-C to break a command pipeline)
396    helpers.setsid().map_err(into_fatal)?;
397
398    // As session leader, we now fork then follow the child again to guarantee
399    // we cannot re-acquire a terminal
400    if helpers.fork().map_err(into_fatal)?.is_parent() {
401        drop(write_fd);
402        exit(0);
403    }
404
405    let pid = getpid();
406    helpers.write_pipe(write_fd, pid).map_err(into_fatal)?;
407
408    // Setup stdio to write errors to the logfile while discarding any IO to
409    // the controlling terminal
410    let fuelup_tmp = helpers
411        .telemetry_config()
412        .map_err(into_fatal)?
413        .fuelup_tmp
414        .clone();
415
416    helpers.setup_stdio(
417        Path::new(&fuelup_tmp)
418            .join(log_filename)
419            .to_str()
420            .ok_or(TelemetryError::InvalidLogFile(
421                fuelup_tmp.clone(),
422                log_filename.clone(),
423            ))
424            .map_err(into_fatal)?,
425    )?;
426
427    // The current working directory needs to be set to root so that we don't
428    // prevent any unmounting of the filesystem leading up to the directory we
429    // started in
430    helpers.chdir(Path::new("/")).map_err(into_fatal)?;
431
432    // We close all file descriptors since any currently opened were inherited
433    // from the parent process which we don't care about. Not doing so leaks
434    // open file descriptors which could lead to exhaustion.
435    let max_fd = helpers
436        .sysconf(SysconfVar::OPEN_MAX)
437        .map_err(into_fatal)?
438        .unwrap_or(MIN_OPEN_MAX.into()) as i32;
439
440    for fd in FIRST_NON_STDIO_FD..=max_fd {
441        match helpers.close(fd) {
442            Ok(()) | Err(Errno::EBADF) => {}
443            Err(e) => Err(into_fatal(e))?,
444        }
445    }
446
447    // Clear the umask so that files we create aren't too permission-restricive
448    stat::umask(stat::Mode::empty());
449
450    Ok(None)
451}
452
453trait DaemoniseHelpers {
454    fn flush<T: Write + std::os::fd::AsRawFd>(
455        &mut self,
456        stream: &mut T,
457    ) -> std::result::Result<(), std::io::Error> {
458        stream.flush()
459    }
460
461    fn pipe(&mut self) -> nix::Result<(OwnedFd, OwnedFd)> {
462        pipe()
463    }
464
465    fn read_pipe(&mut self, read_fd: OwnedFd, pid_bytes: &mut [u8]) -> nix::Result<usize> {
466        read(read_fd.as_raw_fd(), pid_bytes)
467    }
468
469    fn fork(&mut self) -> nix::Result<ForkResult> {
470        unsafe { fork() }
471    }
472
473    fn setsid(&self) -> nix::Result<Pid> {
474        setsid()
475    }
476
477    fn write_pipe(&mut self, write_fd: OwnedFd, pid: Pid) -> nix::Result<usize> {
478        write(write_fd, &pid.as_raw().to_ne_bytes())
479    }
480
481    fn telemetry_config(&mut self) -> Result<&'static TelemetryConfig> {
482        telemetry_config()
483    }
484
485    fn setup_stdio(&self, log_filename: &str) -> std::result::Result<(), TelemetryError> {
486        setup_stdio(log_filename)
487    }
488
489    fn chdir(&self, path: &Path) -> nix::Result<()> {
490        chdir(path)
491    }
492
493    fn sysconf(&self, var: SysconfVar) -> nix::Result<Option<c_long>> {
494        sysconf(var)
495    }
496
497    fn close(&self, fd: c_int) -> nix::Result<()> {
498        close(fd)
499    }
500}
501
502struct DefaultDaemoniseHelpers;
503impl DaemoniseHelpers for DefaultDaemoniseHelpers {}
504
505trait SetupStdioHelpers {
506    fn create_append(&self, log_filename: &str) -> std::result::Result<File, std::io::Error> {
507        OpenOptions::new()
508            .create(true)
509            .append(true)
510            .open(log_filename)
511    }
512
513    fn dup2(&mut self, fd: c_int, fd2: c_int) -> std::result::Result<c_int, nix::errno::Errno> {
514        dup2(fd, fd2)
515    }
516
517    fn read_write(&self, path: &str) -> std::result::Result<File, std::io::Error> {
518        OpenOptions::new().read(true).write(true).open(path)
519    }
520}
521
522struct DefaultSetupStdioHelpers;
523impl SetupStdioHelpers for DefaultSetupStdioHelpers {}
524
525/// Setup stdio for the process
526///
527/// This function redirects stderr to its logfile while discarding any IO to the
528/// controlling terminal.
529pub(crate) fn setup_stdio(log_filename: &str) -> std::result::Result<(), TelemetryError> {
530    let mut helpers = DefaultSetupStdioHelpers;
531    setup_stdio_with_helpers(log_filename, &mut helpers)
532}
533
534fn setup_stdio_with_helpers(
535    log_filename: &str,
536    helpers: &mut impl SetupStdioHelpers,
537) -> std::result::Result<(), TelemetryError> {
538    let log_file = helpers.create_append(log_filename)?;
539
540    // Redirect stderr to the logfile
541    helpers.dup2(log_file.as_raw_fd(), 2)?;
542
543    // Get a filehandle to /dev/null
544    let dev_null = helpers.read_write("/dev/null")?;
545
546    // Redirect stdin, stdout to /dev/null
547    helpers.dup2(dev_null.as_raw_fd(), 0)?;
548    helpers.dup2(dev_null.as_raw_fd(), 1)?;
549
550    Ok(())
551}
552
553#[cfg(test)]
554fn setup_fuelup_home() {
555    let tmp_dir = std::env::temp_dir().join(format!("fuelup-test-{}", uuid::Uuid::new_v4()));
556    std::fs::create_dir_all(&tmp_dir).unwrap();
557    std::env::set_var("FUELUP_HOME", tmp_dir.to_str().unwrap());
558}
559
560#[cfg(test)]
561mod env_setting {
562    use super::*;
563    use std::env::set_var;
564
565    #[test]
566    fn unset() {
567        let env_setting = EnvSetting::new("does_not_exist", "default_value");
568        assert_eq!(env_setting.get(), "default_value");
569    }
570
571    #[test]
572    fn set() {
573        set_var("existing_variable", "existing_value");
574
575        let env_setting = EnvSetting::new("existing_variable", "default_value");
576        assert_eq!(env_setting.get(), "existing_value");
577    }
578}
579
580#[cfg(test)]
581mod telemetry_config {
582    use super::*;
583    use rusty_fork::rusty_fork_test;
584    use std::{env::set_var, path::Path};
585
586    rusty_fork_test! {
587        #[test]
588        fn fuelup_all_unset() {
589            let telemetry_config = telemetry_config().unwrap();
590            let fuelup_home = home_dir().unwrap();
591
592            assert_eq!(
593                telemetry_config.fuelup_tmp,
594                fuelup_home.join(".fuelup/tmp").to_str().unwrap()
595            );
596
597            assert_eq!(
598                telemetry_config.fuelup_log,
599                fuelup_home.join(".fuelup/log").to_str().unwrap()
600            );
601
602            assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
603            assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
604        }
605
606        #[test]
607        fn fuelup_home_set() {
608            setup_fuelup_home();
609
610            let tempdir = var("FUELUP_HOME").unwrap();
611            let telemetry_config = telemetry_config().unwrap();
612
613            assert_eq!(telemetry_config.fuelup_tmp, format!("{}/tmp", tempdir));
614            assert_eq!(telemetry_config.fuelup_log, format!("{}/log", tempdir));
615
616            assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
617            assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
618        }
619
620        #[test]
621        fn fuelup_tmp_set() {
622            let tmpdir = std::env::temp_dir().join(format!("fuelup-test-{}", uuid::Uuid::new_v4()));
623            set_var("FUELUP_TMP", tmpdir.to_str().unwrap());
624            std::fs::create_dir_all(&tmpdir).unwrap();
625
626            let telemetry_config = telemetry_config().unwrap();
627
628            assert_eq!(telemetry_config.fuelup_tmp, tmpdir.to_str().unwrap());
629
630            assert_eq!(
631                telemetry_config.fuelup_log,
632                home_dir().unwrap().join(".fuelup/log").to_str().unwrap()
633            );
634
635            assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
636            assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
637        }
638
639        #[test]
640        fn fuelup_log_set() {
641            let tmpdir = std::env::temp_dir().join(format!("fuelup-test-{}", uuid::Uuid::new_v4()));
642            std::fs::create_dir_all(&tmpdir).unwrap();
643            set_var("FUELUP_LOG", tmpdir.to_str().unwrap());
644
645            let telemetry_config = telemetry_config().unwrap();
646
647            assert_eq!(
648                telemetry_config.fuelup_tmp,
649                home_dir().unwrap().join(".fuelup/tmp").to_str().unwrap()
650            );
651
652            assert_eq!(telemetry_config.fuelup_log, tmpdir.to_str().unwrap());
653
654            assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
655            assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
656        }
657    }
658}
659
660#[cfg(test)]
661mod enforce_singleton {
662    use super::*;
663    use nix::unistd::ForkResult;
664    use rusty_fork::rusty_fork_test;
665    use std::os::fd::OwnedFd;
666
667    fn setup_lockfile() -> PathBuf {
668        let lockfile = format!("{}/test.lock", telemetry_config().unwrap().fuelup_tmp);
669
670        File::create(&lockfile).unwrap();
671        PathBuf::from(lockfile)
672    }
673
674    rusty_fork_test! {
675        #[test]
676        fn lockfile_open_failed() {
677            struct LockfileOpenFailed;
678
679            impl EnforceSingletonHelpers for LockfileOpenFailed {
680                fn open(&self, _filename: &Path) -> std::result::Result<File, std::io::Error> {
681                    Err(std::io::Error::new(
682                        std::io::ErrorKind::NotFound,
683                        "Mock error",
684                    ))
685                }
686            }
687
688            assert_eq!(
689                enforce_singleton_with_helpers(Path::new("test.lock"), &mut LockfileOpenFailed)
690                    .err(),
691                Some(TelemetryError::IO("Mock error".to_string()))
692            );
693        }
694
695        #[test]
696        fn flock_ewouldblock() {
697            setup_fuelup_home();
698            let lockfile = setup_lockfile();
699
700            struct FlockEWouldblock {
701                write_fd: OwnedFd,
702            }
703
704            impl EnforceSingletonHelpers for FlockEWouldblock {
705                fn lock(&self, file: File) -> std::result::Result<Flock<File>, (File, Errno)> {
706                    Err((file, Errno::EWOULDBLOCK))
707                }
708
709                fn exit(&self, _status: i32) -> ! {
710                    // Test we exited at the expected code path
711                    let pid = getpid();
712                    write(&self.write_fd, &pid.as_raw().to_ne_bytes()).unwrap();
713
714                    exit(0);
715                }
716            }
717
718            let (read_fd, write_fd) = pipe().unwrap();
719
720            match unsafe { fork() }.unwrap() {
721                ForkResult::Parent { child } => {
722                    drop(write_fd);
723
724                    let mut pid_bytes = [0u8; std::mem::size_of::<Pid>()];
725                    read(read_fd.as_raw_fd(), &mut pid_bytes).unwrap();
726
727                    assert_eq!(pid_bytes, child.as_raw().to_ne_bytes());
728                }
729                ForkResult::Child => {
730                    drop(read_fd);
731
732                    let mut flock_ewouldblock = FlockEWouldblock { write_fd };
733
734                    enforce_singleton_with_helpers(&lockfile, &mut flock_ewouldblock).unwrap();
735
736                    // Fallback exit, which rusty_fork will catch
737                    exit(99);
738                }
739            }
740        }
741
742        #[test]
743        fn flock_other_error() {
744            setup_fuelup_home();
745            let lockfile = setup_lockfile();
746
747            struct FlockOtherError;
748
749            impl EnforceSingletonHelpers for FlockOtherError {
750                fn lock(&self, file: File) -> std::result::Result<Flock<File>, (File, Errno)> {
751                    Err((file, Errno::EOWNERDEAD))
752                }
753            }
754
755            let result = enforce_singleton_with_helpers(&lockfile, &mut FlockOtherError);
756
757            let expected = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
758            assert_eq!(result.err(), Some(expected));
759        }
760    }
761}
762
763#[cfg(test)]
764mod daemonise {
765    use super::*;
766    use nix::{
767        errno::Errno,
768        sys::wait::{waitpid, WaitStatus},
769        unistd::ForkResult,
770    };
771    use rusty_fork::rusty_fork_test;
772    use std::io::{Error, ErrorKind, Result, Write};
773
774    rusty_fork_test! {
775        #[test]
776        fn stdout_flush_failed() {
777            setup_fuelup_home();
778
779            struct StdoutFlushFailed;
780
781            impl DaemoniseHelpers for StdoutFlushFailed {
782                fn flush<T: Write + std::os::fd::AsRawFd>(&mut self, stream: &mut T) -> Result<()> {
783                    assert_eq!(stream.as_raw_fd(), 1);
784                    Err(Error::new(ErrorKind::Other, "Error flushing stdout"))
785                }
786            }
787
788            let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut StdoutFlushFailed);
789            assert!(matches!(result, Err(WatcherError::Recoverable(_))));
790        }
791
792        #[test]
793        fn stderr_flush_failed() {
794            setup_fuelup_home();
795
796            #[derive(Default)]
797            struct StderrFlushFailed {
798                call_counter: usize,
799            }
800
801            impl DaemoniseHelpers for StderrFlushFailed {
802                fn flush<T: Write + std::os::fd::AsRawFd>(&mut self, stream: &mut T) -> Result<()> {
803                    self.call_counter += 1;
804
805                    if self.call_counter == 1 {
806                        Ok(())
807                    } else {
808                        assert_eq!(stream.as_raw_fd(), 2);
809                        Err(Error::new(ErrorKind::Other, "Error flushing stderr"))
810                    }
811                }
812            }
813
814            let result = daemonise_with_helpers(
815                &PathBuf::from("test.log"),
816                &mut StderrFlushFailed::default(),
817            );
818
819            assert!(matches!(result, Err(WatcherError::Recoverable(_))));
820        }
821
822        #[test]
823        fn pipe_failed() {
824            setup_fuelup_home();
825
826            struct PipeFailed;
827
828            impl DaemoniseHelpers for PipeFailed {
829                fn pipe(&mut self) -> nix::Result<(OwnedFd, OwnedFd)> {
830                    Err(Errno::EOWNERDEAD)
831                }
832            }
833
834            let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut PipeFailed);
835            let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
836
837            assert_eq!(
838                result.err(),
839                Some(WatcherError::Recoverable(expected_error))
840            );
841        }
842
843        #[test]
844        fn first_fork_failed() {
845            setup_fuelup_home();
846
847            struct FirstForkFailed;
848
849            impl DaemoniseHelpers for FirstForkFailed {
850                fn fork(&mut self) -> nix::Result<ForkResult> {
851                    Err(Errno::EOWNERDEAD)
852                }
853            }
854
855            assert_eq!(
856                daemonise_with_helpers(&PathBuf::from("test.log"), &mut FirstForkFailed),
857                Err(WatcherError::Recoverable(TelemetryError::Nix(
858                    Errno::EOWNERDEAD.to_string()
859                )))
860            );
861        }
862
863        #[test]
864        fn first_fork_is_parent() {
865            setup_fuelup_home();
866
867            struct FirstForkIsParent;
868
869            impl DaemoniseHelpers for FirstForkIsParent {
870                fn fork(&mut self) -> nix::Result<ForkResult> {
871                    Ok(ForkResult::Parent {
872                        child: Pid::from_raw(1),
873                    })
874                }
875            }
876
877            let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut FirstForkIsParent);
878            assert!(matches!(result, Ok(Some(_))));
879        }
880
881        #[test]
882        fn second_fork_failed() {
883            setup_fuelup_home();
884
885            #[derive(Default)]
886            struct SecondForkFailed {
887                call_counter: usize,
888            }
889
890            impl DaemoniseHelpers for SecondForkFailed {
891                fn fork(&mut self) -> nix::Result<ForkResult> {
892                    self.call_counter += 1;
893
894                    if self.call_counter == 2 {
895                        Err(Errno::EOWNERDEAD)
896                    } else {
897                        Ok(ForkResult::Child)
898                    }
899                }
900            }
901
902            let result = daemonise_with_helpers(
903                &PathBuf::from("test.log"),
904                &mut SecondForkFailed::default(),
905            );
906
907            assert!(matches!(result, Err(WatcherError::Fatal(_))));
908        }
909
910        #[test]
911        fn second_fork_is_parent() {
912            setup_fuelup_home();
913
914            #[derive(Default)]
915            struct SecondForkIsParent {
916                call_counter: usize,
917            }
918
919            impl DaemoniseHelpers for SecondForkIsParent {
920                fn fork(&mut self) -> nix::Result<ForkResult> {
921                    self.call_counter += 1;
922
923                    if self.call_counter == 2 {
924                        Ok(ForkResult::Parent {
925                            child: Pid::from_raw(1),
926                        })
927                    } else {
928                        Ok(ForkResult::Child)
929                    }
930                }
931            }
932
933            // We ourselves fork so that we can `waitpid` on the function
934            match unsafe { fork() }.unwrap() {
935                ForkResult::Parent { child } => match waitpid(child, None).unwrap() {
936                    WaitStatus::Exited(_, code) => {
937                        assert_eq!(code, 0);
938                    }
939                    _ => panic!("Child did not exit normally"),
940                },
941                ForkResult::Child => {
942                    let _ = daemonise_with_helpers(
943                        &PathBuf::from("test.log"),
944                        &mut SecondForkIsParent::default(),
945                    );
946
947                    // Fallback exit
948                    exit(99);
949                }
950            }
951        }
952
953        #[test]
954        fn setsid_failed() {
955            setup_fuelup_home();
956
957            struct SetsidFailed;
958
959            impl DaemoniseHelpers for SetsidFailed {
960                fn setsid(&self) -> nix::Result<Pid> {
961                    Err(Errno::EOWNERDEAD)
962                }
963
964                // Need to become the child so we don't return as the parent
965                fn fork(&mut self) -> nix::Result<ForkResult> {
966                    Ok(ForkResult::Child)
967                }
968            }
969
970            let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut SetsidFailed);
971
972            let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
973            assert_eq!(result.err(), Some(WatcherError::Fatal(expected_error)));
974        }
975
976        #[test]
977        fn third_fork_failed() {
978            setup_fuelup_home();
979
980            #[derive(Default)]
981            struct ThirdForkFailed {
982                call_counter: usize,
983            }
984
985            impl DaemoniseHelpers for ThirdForkFailed {
986                fn fork(&mut self) -> nix::Result<ForkResult> {
987                    self.call_counter += 1;
988
989                    if self.call_counter == 3 {
990                        Err(Errno::EOWNERDEAD)
991                    } else {
992                        Ok(ForkResult::Child)
993                    }
994                }
995            }
996
997            let result =
998                daemonise_with_helpers(&PathBuf::from("test.log"), &mut ThirdForkFailed::default());
999
1000            let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
1001            assert_eq!(result.err(), Some(WatcherError::Fatal(expected_error)));
1002        }
1003
1004        #[test]
1005        fn third_fork_is_parent() {
1006            setup_fuelup_home();
1007
1008            #[derive(Default)]
1009            struct ThirdForkIsParent {
1010                call_counter: usize,
1011            }
1012
1013            impl DaemoniseHelpers for ThirdForkIsParent {
1014                fn fork(&mut self) -> nix::Result<ForkResult> {
1015                    self.call_counter += 1;
1016
1017                    if self.call_counter == 3 {
1018                        Ok(ForkResult::Parent {
1019                            child: Pid::from_raw(1),
1020                        })
1021                    } else {
1022                        Ok(ForkResult::Child)
1023                    }
1024                }
1025            }
1026
1027            // We ourselves fork so that we can `waitpid` on the function
1028            match unsafe { fork() }.unwrap() {
1029                ForkResult::Parent { child } => match waitpid(child, None).unwrap() {
1030                    WaitStatus::Exited(_, code) => {
1031                        assert_eq!(code, 0);
1032                    }
1033                    _ => panic!("Child did not exit normally"),
1034                },
1035                ForkResult::Child => {
1036                    let _ = daemonise_with_helpers(
1037                        &PathBuf::from("test.log"),
1038                        &mut ThirdForkIsParent::default(),
1039                    );
1040
1041                    // Fallback exit
1042                    exit(99);
1043                }
1044            }
1045        }
1046
1047        #[test]
1048        fn write_pipe_failed() {
1049            setup_fuelup_home();
1050
1051            struct WritePipeFailed;
1052
1053            impl DaemoniseHelpers for WritePipeFailed {
1054                fn write_pipe(&mut self, _write_fd: OwnedFd, _pid: Pid) -> nix::Result<usize> {
1055                    Err(Errno::EOWNERDEAD)
1056                }
1057
1058                fn fork(&mut self) -> nix::Result<ForkResult> {
1059                    Ok(ForkResult::Child)
1060                }
1061            }
1062
1063            let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut WritePipeFailed);
1064
1065            let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
1066            assert_eq!(result.err(), Some(WatcherError::Fatal(expected_error)));
1067        }
1068
1069        #[test]
1070        fn telemetry_config_failed() {
1071            setup_fuelup_home();
1072
1073            struct TelemetryConfigFailed;
1074
1075            impl DaemoniseHelpers for TelemetryConfigFailed {
1076                fn telemetry_config(
1077                    &mut self,
1078                ) -> std::result::Result<&'static TelemetryConfig, errors::TelemetryError>
1079                {
1080                    Err(TelemetryError::Mock)
1081                }
1082
1083                fn fork(&mut self) -> nix::Result<ForkResult> {
1084                    // We want to continue as the child process, so flip the fork result
1085                    // and return the original parent as the child
1086                    let original_parent = getpid();
1087
1088                    match unsafe { fork() }.unwrap() {
1089                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1090                        ForkResult::Child => Ok(ForkResult::Parent {
1091                            child: original_parent,
1092                        }),
1093                    }
1094                }
1095            }
1096
1097            if let Err(e) =
1098                daemonise_with_helpers(&PathBuf::from("test.log"), &mut TelemetryConfigFailed)
1099            {
1100                assert_eq!(e, WatcherError::Fatal(TelemetryError::Mock));
1101            }
1102        }
1103
1104        #[test]
1105        fn setup_stdio_failed() {
1106            setup_fuelup_home();
1107
1108            struct SetupStdioFailed;
1109
1110            impl DaemoniseHelpers for SetupStdioFailed {
1111                fn setup_stdio(
1112                    &self,
1113                    _log_filename: &str,
1114                ) -> std::result::Result<(), TelemetryError> {
1115                    Err(TelemetryError::IO("Error setting up stdio".to_string()))
1116                }
1117
1118                fn fork(&mut self) -> nix::Result<ForkResult> {
1119                    // We want to continue as the child process, so flip the fork result
1120                    // and return the original parent as the child
1121                    let original_parent = getpid();
1122
1123                    match unsafe { fork() }.unwrap() {
1124                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1125                        ForkResult::Child => Ok(ForkResult::Parent {
1126                            child: original_parent,
1127                        }),
1128                    }
1129                }
1130            }
1131
1132            if let Err(e) =
1133                daemonise_with_helpers(&PathBuf::from("test.log"), &mut SetupStdioFailed)
1134            {
1135                let expected_error = TelemetryError::IO("Error setting up stdio".to_string());
1136                assert_eq!(e, WatcherError::Fatal(expected_error));
1137            }
1138        }
1139
1140        #[test]
1141        fn join_failed() {
1142            setup_fuelup_home();
1143
1144            struct JoinFailed;
1145
1146            impl DaemoniseHelpers for JoinFailed {
1147                fn telemetry_config(
1148                    &mut self,
1149                ) -> std::result::Result<&'static TelemetryConfig, errors::TelemetryError>
1150                {
1151                    pub static _TELEMETRY_CONFIG: LazyLock<Result<TelemetryConfig>> =
1152                        LazyLock::new(|| {
1153                            Ok(TelemetryConfig {
1154                                // Use invalid UTF-8 to trigger the error
1155                                fuelup_tmp: unsafe { String::from_utf8_unchecked(vec![0xFF]) },
1156                                fuelup_log: "".to_string(),
1157                            })
1158                        });
1159
1160                    _TELEMETRY_CONFIG.as_ref().map_err(|_| {
1161                        TelemetryError::InvalidConfig("Error getting telemetry config".to_string())
1162                    })
1163                }
1164
1165                fn setup_stdio(
1166                    &self,
1167                    _log_filename: &str,
1168                ) -> std::result::Result<(), TelemetryError> {
1169                    Ok(())
1170                }
1171
1172                fn fork(&mut self) -> nix::Result<ForkResult> {
1173                    // We want to continue as the child process, so flip the fork result
1174                    // and return the original parent as the child
1175                    let original_parent = getpid();
1176
1177                    match unsafe { fork() }.unwrap() {
1178                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1179                        ForkResult::Child => Ok(ForkResult::Parent {
1180                            child: original_parent,
1181                        }),
1182                    }
1183                }
1184            }
1185
1186            if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut JoinFailed) {
1187                assert!(matches!(
1188                    e,
1189                    WatcherError::Fatal(TelemetryError::InvalidLogFile(_, _))
1190                ));
1191            }
1192        }
1193
1194        #[test]
1195        fn chdir_failed() {
1196            setup_fuelup_home();
1197
1198            struct ChdirFailed;
1199
1200            impl DaemoniseHelpers for ChdirFailed {
1201                fn chdir(&self, _path: &Path) -> nix::Result<()> {
1202                    Err(Errno::EOWNERDEAD)
1203                }
1204
1205                fn setup_stdio(
1206                    &self,
1207                    _log_filename: &str,
1208                ) -> std::result::Result<(), TelemetryError> {
1209                    Ok(())
1210                }
1211
1212                fn fork(&mut self) -> nix::Result<ForkResult> {
1213                    // We want to continue as the child process, so flip the fork result
1214                    // and return the original parent as the child
1215                    let original_parent = getpid();
1216
1217                    match unsafe { fork() }.unwrap() {
1218                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1219                        ForkResult::Child => Ok(ForkResult::Parent {
1220                            child: original_parent,
1221                        }),
1222                    }
1223                }
1224            }
1225
1226            if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut ChdirFailed) {
1227                assert_eq!(
1228                    e,
1229                    WatcherError::Fatal(TelemetryError::Nix(Errno::EOWNERDEAD.to_string()))
1230                );
1231            }
1232        }
1233
1234        #[test]
1235        fn sysconf_failed() {
1236            setup_fuelup_home();
1237
1238            struct SysconfFailed;
1239
1240            impl DaemoniseHelpers for SysconfFailed {
1241                fn sysconf(&self, _var: SysconfVar) -> nix::Result<Option<c_long>> {
1242                    Err(Errno::EOWNERDEAD)
1243                }
1244
1245                fn setup_stdio(
1246                    &self,
1247                    _log_filename: &str,
1248                ) -> std::result::Result<(), TelemetryError> {
1249                    Ok(())
1250                }
1251
1252                fn fork(&mut self) -> nix::Result<ForkResult> {
1253                    // We want to continue as the child process, so flip the fork result
1254                    // and return the original parent as the child
1255                    let original_parent = getpid();
1256
1257                    match unsafe { fork() }.unwrap() {
1258                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1259                        ForkResult::Child => Ok(ForkResult::Parent {
1260                            child: original_parent,
1261                        }),
1262                    }
1263                }
1264            }
1265
1266            if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut SysconfFailed) {
1267                assert_eq!(
1268                    e,
1269                    WatcherError::Fatal(TelemetryError::Nix(Errno::EOWNERDEAD.to_string()))
1270                );
1271            }
1272        }
1273
1274        #[test]
1275        fn close_failed_with_ebadf() {
1276            setup_fuelup_home();
1277
1278            struct CloseFailed;
1279
1280            impl DaemoniseHelpers for CloseFailed {
1281                fn close(&self, _fd: c_int) -> nix::Result<()> {
1282                    Err(Errno::EBADF)
1283                }
1284
1285                fn setup_stdio(
1286                    &self,
1287                    _log_filename: &str,
1288                ) -> std::result::Result<(), TelemetryError> {
1289                    Ok(())
1290                }
1291
1292                fn fork(&mut self) -> nix::Result<ForkResult> {
1293                    // We want to continue as the child process, so flip the fork result
1294                    // and return the original parent as the child
1295                    let original_parent = getpid();
1296
1297                    match unsafe { fork() }.unwrap() {
1298                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1299                        ForkResult::Child => Ok(ForkResult::Parent {
1300                            child: original_parent,
1301                        }),
1302                    }
1303                }
1304            }
1305
1306            if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut CloseFailed) {
1307                assert_eq!(
1308                    e,
1309                    WatcherError::Fatal(TelemetryError::Nix(Errno::EBADF.to_string()))
1310                );
1311            }
1312        }
1313
1314        #[test]
1315        fn close_failed_with_other_error() {
1316            setup_fuelup_home();
1317
1318            struct CloseFailed;
1319
1320            impl DaemoniseHelpers for CloseFailed {
1321                fn close(&self, _fd: c_int) -> nix::Result<()> {
1322                    Err(Errno::EOWNERDEAD)
1323                }
1324
1325                fn setup_stdio(
1326                    &self,
1327                    _log_filename: &str,
1328                ) -> std::result::Result<(), TelemetryError> {
1329                    Ok(())
1330                }
1331
1332                fn fork(&mut self) -> nix::Result<ForkResult> {
1333                    // We want to continue as the child process, so flip the fork result
1334                    // and return the original parent as the child
1335                    let original_parent = getpid();
1336
1337                    match unsafe { fork() }.unwrap() {
1338                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1339                        ForkResult::Child => Ok(ForkResult::Parent {
1340                            child: original_parent,
1341                        }),
1342                    }
1343                }
1344            }
1345
1346            if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut CloseFailed) {
1347                assert_eq!(
1348                    e,
1349                    WatcherError::Fatal(TelemetryError::Nix(Errno::EOWNERDEAD.to_string()))
1350                );
1351            }
1352        }
1353
1354        #[test]
1355        fn ok() {
1356            setup_fuelup_home();
1357
1358            struct AOk;
1359
1360            impl DaemoniseHelpers for AOk {
1361                fn setup_stdio(
1362                    &self,
1363                    _log_filename: &str,
1364                ) -> std::result::Result<(), TelemetryError> {
1365                    Ok(())
1366                }
1367
1368                fn fork(&mut self) -> nix::Result<ForkResult> {
1369                    // We want to continue as the child process, so flip the fork result
1370                    // and return the original parent as the child
1371                    let original_parent = getpid();
1372
1373                    match unsafe { fork() }.unwrap() {
1374                        ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
1375                        ForkResult::Child => Ok(ForkResult::Parent {
1376                            child: original_parent,
1377                        }),
1378                    }
1379                }
1380            }
1381
1382            let parent_pid = getpid();
1383            let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut AOk);
1384
1385            // We only care about the point of view from the parent (with flipped fork result)
1386            if getpid() == parent_pid {
1387                assert_eq!(result, Ok(None));
1388            }
1389        }
1390    }
1391}
1392
1393#[cfg(test)]
1394mod setup_stdio {
1395    use super::*;
1396    use rusty_fork::rusty_fork_test;
1397
1398    rusty_fork_test! {
1399        #[test]
1400        fn create_append_failed() {
1401            setup_fuelup_home();
1402
1403            struct CreateAppendFailed;
1404
1405            impl SetupStdioHelpers for CreateAppendFailed {
1406                fn create_append(
1407                    &self,
1408                    _log_filename: &str,
1409                ) -> std::result::Result<File, std::io::Error> {
1410                    Err(std::io::Error::new(
1411                        std::io::ErrorKind::Other,
1412                        "Error creating append",
1413                    ))
1414                }
1415            }
1416
1417            let result = setup_stdio_with_helpers(
1418                &format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
1419                &mut CreateAppendFailed,
1420            );
1421
1422            assert!(matches!(result, Err(TelemetryError::IO(_))));
1423        }
1424
1425        #[test]
1426        fn first_dup2_failed() {
1427            setup_fuelup_home();
1428
1429            struct FirstDup2Failed;
1430
1431            impl SetupStdioHelpers for FirstDup2Failed {
1432                fn dup2(
1433                    &mut self,
1434                    _fd: c_int,
1435                    fd2: c_int,
1436                ) -> std::result::Result<c_int, nix::errno::Errno> {
1437                    assert_eq!(fd2, 2);
1438                    Err(nix::errno::Errno::EOWNERDEAD)
1439                }
1440            }
1441
1442            let result = setup_stdio_with_helpers(
1443                &format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
1444                &mut FirstDup2Failed,
1445            );
1446
1447            assert!(matches!(result, Err(TelemetryError::Nix(_))));
1448        }
1449
1450        #[test]
1451        fn read_write_failed() {
1452            setup_fuelup_home();
1453
1454            struct ReadWriteFailed;
1455
1456            impl SetupStdioHelpers for ReadWriteFailed {
1457                fn read_write(&self, _path: &str) -> std::result::Result<File, std::io::Error> {
1458                    Err(std::io::Error::new(
1459                        std::io::ErrorKind::Other,
1460                        "Error reading write",
1461                    ))
1462                }
1463            }
1464
1465            let result = setup_stdio_with_helpers(
1466                &format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
1467                &mut ReadWriteFailed,
1468            );
1469
1470            assert!(matches!(result, Err(TelemetryError::IO(_))));
1471        }
1472
1473        #[test]
1474        fn second_dup2_failed() {
1475            setup_fuelup_home();
1476
1477            #[derive(Default)]
1478            struct SecondDup2Failed {
1479                call_counter: usize,
1480            }
1481
1482            impl SetupStdioHelpers for SecondDup2Failed {
1483                fn dup2(
1484                    &mut self,
1485                    _fd: c_int,
1486                    fd2: c_int,
1487                ) -> std::result::Result<c_int, nix::errno::Errno> {
1488                    self.call_counter += 1;
1489
1490                    if self.call_counter == 2 {
1491                        assert_eq!(fd2, 0);
1492                        Err(nix::errno::Errno::EOWNERDEAD)
1493                    } else {
1494                        Ok(0)
1495                    }
1496                }
1497            }
1498
1499            let result = setup_stdio_with_helpers(
1500                &format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
1501                &mut SecondDup2Failed::default(),
1502            );
1503
1504            assert!(matches!(result, Err(TelemetryError::Nix(_))));
1505        }
1506
1507        #[test]
1508        fn third_dup2_failed() {
1509            setup_fuelup_home();
1510
1511            #[derive(Default)]
1512            struct ThirdDup2Failed {
1513                call_counter: usize,
1514            }
1515
1516            impl SetupStdioHelpers for ThirdDup2Failed {
1517                fn dup2(
1518                    &mut self,
1519                    _fd: c_int,
1520                    fd2: c_int,
1521                ) -> std::result::Result<c_int, nix::errno::Errno> {
1522                    self.call_counter += 1;
1523
1524                    if self.call_counter == 3 {
1525                        assert_eq!(fd2, 1);
1526                        Err(nix::errno::Errno::EOWNERDEAD)
1527                    } else {
1528                        Ok(0)
1529                    }
1530                }
1531            }
1532
1533            let result = setup_stdio_with_helpers(
1534                &format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
1535                &mut ThirdDup2Failed::default(),
1536            );
1537
1538            assert!(matches!(result, Err(TelemetryError::Nix(_))));
1539        }
1540
1541        #[test]
1542        fn ok() {
1543            setup_fuelup_home();
1544
1545            let result = setup_stdio_with_helpers(
1546                &format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
1547                &mut DefaultSetupStdioHelpers,
1548            );
1549
1550            assert!(matches!(result, Ok(())));
1551        }
1552    }
1553}