pub mod errors;
pub mod file_watcher;
pub mod process_watcher;
pub mod systeminfo_watcher;
pub mod telemetry_formatter;
pub mod telemetry_layer;
pub use errors::{into_fatal, into_recoverable, TelemetryError, WatcherError};
pub use fuel_telemetry_macros::{new, new_with_watchers, new_with_watchers_and_init};
pub use telemetry_formatter::TelemetryFormatter;
pub use telemetry_layer::TelemetryLayer;
pub use tracing::{debug, error, event, info, span, trace, warn, Level};
pub use tracing_appender::non_blocking::WorkerGuard;
pub mod prelude {
pub use crate::{
debug, debug_telemetry, error, error_telemetry, event, info, info_telemetry, span,
span_telemetry, trace, trace_telemetry, warn, warn_telemetry, Level, TelemetryLayer,
};
}
pub use tracing as __reexport_tracing;
pub use tracing_subscriber as __reexport_tracing_subscriber;
pub use tracing_subscriber::filter::EnvFilter as __reexport_EnvFilter;
pub use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt as __reexport_tracing_subscriber_SubscriberExt;
pub use tracing_subscriber::util::SubscriberInitExt as __reexport_SubscriberInitExt;
pub use tracing_subscriber::Layer as __reexport_Layer;
use dirs::home_dir;
use libc::{c_int, c_long};
use nix::{
errno::Errno,
fcntl::{Flock, FlockArg},
sys::stat,
unistd::{
chdir, close, dup2, fork, getpid, pipe, read, setsid, sysconf, write, ForkResult, Pid,
SysconfVar,
},
};
use std::{
env::{current_exe, var, var_os},
fs::{create_dir_all, File, OpenOptions},
io::{stderr, stdout, Write},
os::fd::{AsRawFd, OwnedFd},
path::{Path, PathBuf},
process::exit,
sync::LazyLock,
};
const FIRST_NON_STDIO_FD: i32 = 3;
const MIN_OPEN_MAX: i32 = 1024;
pub type Result<T> = std::result::Result<T, TelemetryError>;
pub type WatcherResult<T> = std::result::Result<T, WatcherError>;
pub struct EnvSetting {
name: &'static str,
default: &'static str,
}
impl EnvSetting {
pub fn new(name: &'static str, default: &'static str) -> Self {
Self { name, default }
}
pub fn get(&self) -> String {
var(self.name).unwrap_or_else(|_| self.default.to_string())
}
}
pub struct TelemetryConfig {
fuelup_tmp: String,
fuelup_log: String,
}
pub fn telemetry_config() -> Result<&'static TelemetryConfig> {
pub static TELEMETRY_CONFIG: LazyLock<Result<TelemetryConfig>> = LazyLock::new(|| {
let fuelup_home_env = EnvSetting {
name: "FUELUP_HOME",
default: ".fuelup",
};
let fuelup_tmp_env = EnvSetting {
name: "FUELUP_TMP",
default: "tmp",
};
let fuelup_log_env = EnvSetting {
name: "FUELUP_LOG",
default: "log",
};
let fuelup_home = var_os(fuelup_home_env.name)
.map(PathBuf::from)
.or_else(|| home_dir().map(|dir| dir.join(fuelup_home_env.default)))
.ok_or(TelemetryError::UnreachableHomeDir)?
.into_os_string()
.into_string()
.map_err(|e| TelemetryError::InvalidHomeDir(e.to_string_lossy().into()))?;
let fuelup_tmp = var_os(fuelup_tmp_env.name)
.unwrap_or_else(|| {
PathBuf::from(fuelup_home.clone())
.join(fuelup_tmp_env.default)
.into_os_string()
})
.into_string()
.map_err(|e| TelemetryError::InvalidTmpDir(e.to_string_lossy().into()))?;
let fuelup_log = var_os(fuelup_log_env.name)
.unwrap_or_else(|| {
PathBuf::from(fuelup_home.clone())
.join(fuelup_log_env.default)
.into_os_string()
})
.into_string()
.map_err(|e| TelemetryError::InvalidLogDir(e.to_string_lossy().into()))?;
create_dir_all(&fuelup_tmp)?;
create_dir_all(&fuelup_log)?;
Ok(TelemetryConfig {
fuelup_tmp,
fuelup_log,
})
});
TELEMETRY_CONFIG
.as_ref()
.map_err(|e| TelemetryError::InvalidConfig(e.to_string()))
}
#[macro_export]
macro_rules! span_telemetry {
($level:expr, $($arg:tt)*) => {
$crate::__reexport_tracing::span!($level, "auto", telemetry = true).in_scope(|| {
$crate::__reexport_tracing::event!($level, $($arg)*)
})
}
}
#[macro_export]
macro_rules! error_telemetry {
($($arg:tt)*) => {{
span_telemetry!($crate::__reexport_tracing::Level::ERROR, $($arg)*);
}}
}
#[macro_export]
macro_rules! warn_telemetry {
($($arg:tt)*) => {{
span_telemetry!($crate::__reexport_tracing::Level::WARN, $($arg)*);
}}
}
#[macro_export]
macro_rules! info_telemetry {
($($arg:tt)*) => {{
span_telemetry!($crate::__reexport_tracing::Level::INFO, $($arg)*);
}}
}
#[macro_export]
macro_rules! debug_telemetry {
($($arg:tt)*) => {{
span_telemetry!($crate::__reexport_tracing::Level::DEBUG, $($arg)*);
}}
}
#[macro_export]
macro_rules! trace_telemetry {
($($arg:tt)*) => {{
span_telemetry!($crate::__reexport_tracing::Level::TRACE, $($arg)*);
}}
}
pub fn get_process_name() -> String {
let mut exe_name = String::from("unknown");
if let Ok(exe) = current_exe() {
if let Some(name) = exe.file_name() {
if let Some(name_str) = name.to_str() {
exe_name = name_str.to_string().replace(':', "_");
}
}
}
exe_name
}
pub(crate) fn enforce_singleton(filename: &Path) -> Result<Flock<File>> {
enforce_singleton_with_helpers(filename, &mut DefaultEnforceSingletonHelpers)
}
fn enforce_singleton_with_helpers(
filename: &Path,
helpers: &mut impl EnforceSingletonHelpers,
) -> Result<Flock<File>> {
let lockfile = helpers.open(filename)?;
let lock = match helpers.lock(lockfile) {
Ok(lock) => lock,
Err((_, Errno::EWOULDBLOCK)) => {
helpers.exit(0);
}
Err((_, e)) => return Err(TelemetryError::from(e)),
};
Ok(lock)
}
trait EnforceSingletonHelpers {
fn open(&self, filename: &Path) -> std::result::Result<File, std::io::Error> {
OpenOptions::new().create(true).append(true).open(filename)
}
fn lock(&self, file: File) -> std::result::Result<Flock<File>, (File, Errno)> {
Flock::lock(file, FlockArg::LockExclusiveNonblock)
}
fn exit(&self, status: i32) -> ! {
exit(status)
}
}
struct DefaultEnforceSingletonHelpers;
impl EnforceSingletonHelpers for DefaultEnforceSingletonHelpers {}
pub(crate) fn daemonise(log_filename: &PathBuf) -> WatcherResult<Option<Pid>> {
let mut helpers = DefaultDaemoniseHelpers;
daemonise_with_helpers(log_filename, &mut helpers)
}
fn daemonise_with_helpers(
log_filename: &PathBuf,
helpers: &mut impl DaemoniseHelpers,
) -> WatcherResult<Option<Pid>> {
helpers.flush(&mut stdout()).map_err(into_recoverable)?;
helpers.flush(&mut stderr()).map_err(into_recoverable)?;
let (read_fd, write_fd) = helpers.pipe().map_err(into_recoverable)?;
if helpers.fork().map_err(into_recoverable)?.is_parent() {
drop(write_fd);
let mut pid_bytes = [0u8; std::mem::size_of::<Pid>()];
helpers
.read_pipe(read_fd, &mut pid_bytes)
.map_err(into_recoverable)?;
return Ok(Some(Pid::from_raw(i32::from_ne_bytes(pid_bytes))));
};
drop(read_fd);
if helpers.fork().map_err(into_fatal)?.is_parent() {
drop(write_fd);
exit(0);
}
helpers.setsid().map_err(into_fatal)?;
if helpers.fork().map_err(into_fatal)?.is_parent() {
drop(write_fd);
exit(0);
}
let pid = getpid();
helpers.write_pipe(write_fd, pid).map_err(into_fatal)?;
let fuelup_tmp = helpers
.telemetry_config()
.map_err(into_fatal)?
.fuelup_tmp
.clone();
helpers.setup_stdio(
Path::new(&fuelup_tmp)
.join(log_filename)
.to_str()
.ok_or(TelemetryError::InvalidLogFile(
fuelup_tmp.clone(),
log_filename.clone(),
))
.map_err(into_fatal)?,
)?;
helpers.chdir(Path::new("/")).map_err(into_fatal)?;
let max_fd = helpers
.sysconf(SysconfVar::OPEN_MAX)
.map_err(into_fatal)?
.unwrap_or(MIN_OPEN_MAX.into()) as i32;
for fd in FIRST_NON_STDIO_FD..=max_fd {
match helpers.close(fd) {
Ok(()) | Err(Errno::EBADF) => {}
Err(e) => Err(into_fatal(e))?,
}
}
stat::umask(stat::Mode::empty());
Ok(None)
}
trait DaemoniseHelpers {
fn flush<T: Write + std::os::fd::AsRawFd>(
&mut self,
stream: &mut T,
) -> std::result::Result<(), std::io::Error> {
stream.flush()
}
fn pipe(&mut self) -> nix::Result<(OwnedFd, OwnedFd)> {
pipe()
}
fn read_pipe(&mut self, read_fd: OwnedFd, pid_bytes: &mut [u8]) -> nix::Result<usize> {
read(read_fd.as_raw_fd(), pid_bytes)
}
fn fork(&mut self) -> nix::Result<ForkResult> {
unsafe { fork() }
}
fn setsid(&self) -> nix::Result<Pid> {
setsid()
}
fn write_pipe(&mut self, write_fd: OwnedFd, pid: Pid) -> nix::Result<usize> {
write(write_fd, &pid.as_raw().to_ne_bytes())
}
fn telemetry_config(&mut self) -> Result<&'static TelemetryConfig> {
telemetry_config()
}
fn setup_stdio(&self, log_filename: &str) -> std::result::Result<(), TelemetryError> {
setup_stdio(log_filename)
}
fn chdir(&self, path: &Path) -> nix::Result<()> {
chdir(path)
}
fn sysconf(&self, var: SysconfVar) -> nix::Result<Option<c_long>> {
sysconf(var)
}
fn close(&self, fd: c_int) -> nix::Result<()> {
close(fd)
}
}
struct DefaultDaemoniseHelpers;
impl DaemoniseHelpers for DefaultDaemoniseHelpers {}
trait SetupStdioHelpers {
fn create_append(&self, log_filename: &str) -> std::result::Result<File, std::io::Error> {
OpenOptions::new()
.create(true)
.append(true)
.open(log_filename)
}
fn dup2(&mut self, fd: c_int, fd2: c_int) -> std::result::Result<c_int, nix::errno::Errno> {
dup2(fd, fd2)
}
fn read_write(&self, path: &str) -> std::result::Result<File, std::io::Error> {
OpenOptions::new().read(true).write(true).open(path)
}
}
struct DefaultSetupStdioHelpers;
impl SetupStdioHelpers for DefaultSetupStdioHelpers {}
pub(crate) fn setup_stdio(log_filename: &str) -> std::result::Result<(), TelemetryError> {
let mut helpers = DefaultSetupStdioHelpers;
setup_stdio_with_helpers(log_filename, &mut helpers)
}
fn setup_stdio_with_helpers(
log_filename: &str,
helpers: &mut impl SetupStdioHelpers,
) -> std::result::Result<(), TelemetryError> {
let log_file = helpers.create_append(log_filename)?;
helpers.dup2(log_file.as_raw_fd(), 2)?;
let dev_null = helpers.read_write("/dev/null")?;
helpers.dup2(dev_null.as_raw_fd(), 0)?;
helpers.dup2(dev_null.as_raw_fd(), 1)?;
Ok(())
}
#[cfg(test)]
fn setup_fuelup_home() {
let tmp_dir = std::env::temp_dir().join(format!("fuelup-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&tmp_dir).unwrap();
std::env::set_var("FUELUP_HOME", tmp_dir.to_str().unwrap());
}
#[cfg(test)]
mod env_setting {
use super::*;
use std::env::set_var;
#[test]
fn unset() {
let env_setting = EnvSetting::new("does_not_exist", "default_value");
assert_eq!(env_setting.get(), "default_value");
}
#[test]
fn set() {
set_var("existing_variable", "existing_value");
let env_setting = EnvSetting::new("existing_variable", "default_value");
assert_eq!(env_setting.get(), "existing_value");
}
}
#[cfg(test)]
mod telemetry_config {
use super::*;
use rusty_fork::rusty_fork_test;
use std::{env::set_var, path::Path};
rusty_fork_test! {
#[test]
fn fuelup_all_unset() {
let telemetry_config = telemetry_config().unwrap();
let fuelup_home = home_dir().unwrap();
assert_eq!(
telemetry_config.fuelup_tmp,
fuelup_home.join(".fuelup/tmp").to_str().unwrap()
);
assert_eq!(
telemetry_config.fuelup_log,
fuelup_home.join(".fuelup/log").to_str().unwrap()
);
assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
}
#[test]
fn fuelup_home_set() {
setup_fuelup_home();
let tempdir = var("FUELUP_HOME").unwrap();
let telemetry_config = telemetry_config().unwrap();
assert_eq!(telemetry_config.fuelup_tmp, format!("{}/tmp", tempdir));
assert_eq!(telemetry_config.fuelup_log, format!("{}/log", tempdir));
assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
}
#[test]
fn fuelup_tmp_set() {
let tmpdir = std::env::temp_dir().join(format!("fuelup-test-{}", uuid::Uuid::new_v4()));
set_var("FUELUP_TMP", tmpdir.to_str().unwrap());
std::fs::create_dir_all(&tmpdir).unwrap();
let telemetry_config = telemetry_config().unwrap();
assert_eq!(telemetry_config.fuelup_tmp, tmpdir.to_str().unwrap());
assert_eq!(
telemetry_config.fuelup_log,
home_dir().unwrap().join(".fuelup/log").to_str().unwrap()
);
assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
}
#[test]
fn fuelup_log_set() {
let tmpdir = std::env::temp_dir().join(format!("fuelup-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&tmpdir).unwrap();
set_var("FUELUP_LOG", tmpdir.to_str().unwrap());
let telemetry_config = telemetry_config().unwrap();
assert_eq!(
telemetry_config.fuelup_tmp,
home_dir().unwrap().join(".fuelup/tmp").to_str().unwrap()
);
assert_eq!(telemetry_config.fuelup_log, tmpdir.to_str().unwrap());
assert!(Path::new(&telemetry_config.fuelup_tmp).is_dir());
assert!(Path::new(&telemetry_config.fuelup_log).is_dir());
}
}
}
#[cfg(test)]
mod enforce_singleton {
use super::*;
use nix::unistd::ForkResult;
use rusty_fork::rusty_fork_test;
use std::os::fd::OwnedFd;
fn setup_lockfile() -> PathBuf {
let lockfile = format!("{}/test.lock", telemetry_config().unwrap().fuelup_tmp);
File::create(&lockfile).unwrap();
PathBuf::from(lockfile)
}
rusty_fork_test! {
#[test]
fn lockfile_open_failed() {
struct LockfileOpenFailed;
impl EnforceSingletonHelpers for LockfileOpenFailed {
fn open(&self, _filename: &Path) -> std::result::Result<File, std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Mock error",
))
}
}
assert_eq!(
enforce_singleton_with_helpers(Path::new("test.lock"), &mut LockfileOpenFailed)
.err(),
Some(TelemetryError::IO("Mock error".to_string()))
);
}
#[test]
fn flock_ewouldblock() {
setup_fuelup_home();
let lockfile = setup_lockfile();
struct FlockEWouldblock {
write_fd: OwnedFd,
}
impl EnforceSingletonHelpers for FlockEWouldblock {
fn lock(&self, file: File) -> std::result::Result<Flock<File>, (File, Errno)> {
Err((file, Errno::EWOULDBLOCK))
}
fn exit(&self, _status: i32) -> ! {
let pid = getpid();
write(&self.write_fd, &pid.as_raw().to_ne_bytes()).unwrap();
exit(0);
}
}
let (read_fd, write_fd) = pipe().unwrap();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child } => {
drop(write_fd);
let mut pid_bytes = [0u8; std::mem::size_of::<Pid>()];
read(read_fd.as_raw_fd(), &mut pid_bytes).unwrap();
assert_eq!(pid_bytes, child.as_raw().to_ne_bytes());
}
ForkResult::Child => {
drop(read_fd);
let mut flock_ewouldblock = FlockEWouldblock { write_fd };
enforce_singleton_with_helpers(&lockfile, &mut flock_ewouldblock).unwrap();
exit(99);
}
}
}
#[test]
fn flock_other_error() {
setup_fuelup_home();
let lockfile = setup_lockfile();
struct FlockOtherError;
impl EnforceSingletonHelpers for FlockOtherError {
fn lock(&self, file: File) -> std::result::Result<Flock<File>, (File, Errno)> {
Err((file, Errno::EOWNERDEAD))
}
}
let result = enforce_singleton_with_helpers(&lockfile, &mut FlockOtherError);
let expected = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
assert_eq!(result.err(), Some(expected));
}
}
}
#[cfg(test)]
mod daemonise {
use super::*;
use nix::{
errno::Errno,
sys::wait::{waitpid, WaitStatus},
unistd::ForkResult,
};
use rusty_fork::rusty_fork_test;
use std::io::{Error, ErrorKind, Result, Write};
rusty_fork_test! {
#[test]
fn stdout_flush_failed() {
setup_fuelup_home();
struct StdoutFlushFailed;
impl DaemoniseHelpers for StdoutFlushFailed {
fn flush<T: Write + std::os::fd::AsRawFd>(&mut self, stream: &mut T) -> Result<()> {
assert_eq!(stream.as_raw_fd(), 1);
Err(Error::new(ErrorKind::Other, "Error flushing stdout"))
}
}
let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut StdoutFlushFailed);
assert!(matches!(result, Err(WatcherError::Recoverable(_))));
}
#[test]
fn stderr_flush_failed() {
setup_fuelup_home();
#[derive(Default)]
struct StderrFlushFailed {
call_counter: usize,
}
impl DaemoniseHelpers for StderrFlushFailed {
fn flush<T: Write + std::os::fd::AsRawFd>(&mut self, stream: &mut T) -> Result<()> {
self.call_counter += 1;
if self.call_counter == 1 {
Ok(())
} else {
assert_eq!(stream.as_raw_fd(), 2);
Err(Error::new(ErrorKind::Other, "Error flushing stderr"))
}
}
}
let result = daemonise_with_helpers(
&PathBuf::from("test.log"),
&mut StderrFlushFailed::default(),
);
assert!(matches!(result, Err(WatcherError::Recoverable(_))));
}
#[test]
fn pipe_failed() {
setup_fuelup_home();
struct PipeFailed;
impl DaemoniseHelpers for PipeFailed {
fn pipe(&mut self) -> nix::Result<(OwnedFd, OwnedFd)> {
Err(Errno::EOWNERDEAD)
}
}
let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut PipeFailed);
let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
assert_eq!(
result.err(),
Some(WatcherError::Recoverable(expected_error))
);
}
#[test]
fn first_fork_failed() {
setup_fuelup_home();
struct FirstForkFailed;
impl DaemoniseHelpers for FirstForkFailed {
fn fork(&mut self) -> nix::Result<ForkResult> {
Err(Errno::EOWNERDEAD)
}
}
assert_eq!(
daemonise_with_helpers(&PathBuf::from("test.log"), &mut FirstForkFailed),
Err(WatcherError::Recoverable(TelemetryError::Nix(
Errno::EOWNERDEAD.to_string()
)))
);
}
#[test]
fn first_fork_is_parent() {
setup_fuelup_home();
struct FirstForkIsParent;
impl DaemoniseHelpers for FirstForkIsParent {
fn fork(&mut self) -> nix::Result<ForkResult> {
Ok(ForkResult::Parent {
child: Pid::from_raw(1),
})
}
}
let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut FirstForkIsParent);
assert!(matches!(result, Ok(Some(_))));
}
#[test]
fn second_fork_failed() {
setup_fuelup_home();
#[derive(Default)]
struct SecondForkFailed {
call_counter: usize,
}
impl DaemoniseHelpers for SecondForkFailed {
fn fork(&mut self) -> nix::Result<ForkResult> {
self.call_counter += 1;
if self.call_counter == 2 {
Err(Errno::EOWNERDEAD)
} else {
Ok(ForkResult::Child)
}
}
}
let result = daemonise_with_helpers(
&PathBuf::from("test.log"),
&mut SecondForkFailed::default(),
);
assert!(matches!(result, Err(WatcherError::Fatal(_))));
}
#[test]
fn second_fork_is_parent() {
setup_fuelup_home();
#[derive(Default)]
struct SecondForkIsParent {
call_counter: usize,
}
impl DaemoniseHelpers for SecondForkIsParent {
fn fork(&mut self) -> nix::Result<ForkResult> {
self.call_counter += 1;
if self.call_counter == 2 {
Ok(ForkResult::Parent {
child: Pid::from_raw(1),
})
} else {
Ok(ForkResult::Child)
}
}
}
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child } => match waitpid(child, None).unwrap() {
WaitStatus::Exited(_, code) => {
assert_eq!(code, 0);
}
_ => panic!("Child did not exit normally"),
},
ForkResult::Child => {
let _ = daemonise_with_helpers(
&PathBuf::from("test.log"),
&mut SecondForkIsParent::default(),
);
exit(99);
}
}
}
#[test]
fn setsid_failed() {
setup_fuelup_home();
struct SetsidFailed;
impl DaemoniseHelpers for SetsidFailed {
fn setsid(&self) -> nix::Result<Pid> {
Err(Errno::EOWNERDEAD)
}
fn fork(&mut self) -> nix::Result<ForkResult> {
Ok(ForkResult::Child)
}
}
let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut SetsidFailed);
let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
assert_eq!(result.err(), Some(WatcherError::Fatal(expected_error)));
}
#[test]
fn third_fork_failed() {
setup_fuelup_home();
#[derive(Default)]
struct ThirdForkFailed {
call_counter: usize,
}
impl DaemoniseHelpers for ThirdForkFailed {
fn fork(&mut self) -> nix::Result<ForkResult> {
self.call_counter += 1;
if self.call_counter == 3 {
Err(Errno::EOWNERDEAD)
} else {
Ok(ForkResult::Child)
}
}
}
let result =
daemonise_with_helpers(&PathBuf::from("test.log"), &mut ThirdForkFailed::default());
let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
assert_eq!(result.err(), Some(WatcherError::Fatal(expected_error)));
}
#[test]
fn third_fork_is_parent() {
setup_fuelup_home();
#[derive(Default)]
struct ThirdForkIsParent {
call_counter: usize,
}
impl DaemoniseHelpers for ThirdForkIsParent {
fn fork(&mut self) -> nix::Result<ForkResult> {
self.call_counter += 1;
if self.call_counter == 3 {
Ok(ForkResult::Parent {
child: Pid::from_raw(1),
})
} else {
Ok(ForkResult::Child)
}
}
}
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child } => match waitpid(child, None).unwrap() {
WaitStatus::Exited(_, code) => {
assert_eq!(code, 0);
}
_ => panic!("Child did not exit normally"),
},
ForkResult::Child => {
let _ = daemonise_with_helpers(
&PathBuf::from("test.log"),
&mut ThirdForkIsParent::default(),
);
exit(99);
}
}
}
#[test]
fn write_pipe_failed() {
setup_fuelup_home();
struct WritePipeFailed;
impl DaemoniseHelpers for WritePipeFailed {
fn write_pipe(&mut self, _write_fd: OwnedFd, _pid: Pid) -> nix::Result<usize> {
Err(Errno::EOWNERDEAD)
}
fn fork(&mut self) -> nix::Result<ForkResult> {
Ok(ForkResult::Child)
}
}
let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut WritePipeFailed);
let expected_error = TelemetryError::Nix(Errno::EOWNERDEAD.to_string());
assert_eq!(result.err(), Some(WatcherError::Fatal(expected_error)));
}
#[test]
fn telemetry_config_failed() {
setup_fuelup_home();
struct TelemetryConfigFailed;
impl DaemoniseHelpers for TelemetryConfigFailed {
fn telemetry_config(
&mut self,
) -> std::result::Result<&'static TelemetryConfig, errors::TelemetryError>
{
Err(TelemetryError::Mock)
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
if let Err(e) =
daemonise_with_helpers(&PathBuf::from("test.log"), &mut TelemetryConfigFailed)
{
assert_eq!(e, WatcherError::Fatal(TelemetryError::Mock));
}
}
#[test]
fn setup_stdio_failed() {
setup_fuelup_home();
struct SetupStdioFailed;
impl DaemoniseHelpers for SetupStdioFailed {
fn setup_stdio(
&self,
_log_filename: &str,
) -> std::result::Result<(), TelemetryError> {
Err(TelemetryError::IO("Error setting up stdio".to_string()))
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
if let Err(e) =
daemonise_with_helpers(&PathBuf::from("test.log"), &mut SetupStdioFailed)
{
let expected_error = TelemetryError::IO("Error setting up stdio".to_string());
assert_eq!(e, WatcherError::Fatal(expected_error));
}
}
#[test]
fn join_failed() {
setup_fuelup_home();
struct JoinFailed;
impl DaemoniseHelpers for JoinFailed {
fn telemetry_config(
&mut self,
) -> std::result::Result<&'static TelemetryConfig, errors::TelemetryError>
{
pub static _TELEMETRY_CONFIG: LazyLock<Result<TelemetryConfig>> =
LazyLock::new(|| {
Ok(TelemetryConfig {
fuelup_tmp: unsafe { String::from_utf8_unchecked(vec![0xFF]) },
fuelup_log: "".to_string(),
})
});
_TELEMETRY_CONFIG.as_ref().map_err(|_| {
TelemetryError::InvalidConfig("Error getting telemetry config".to_string())
})
}
fn setup_stdio(
&self,
_log_filename: &str,
) -> std::result::Result<(), TelemetryError> {
Ok(())
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut JoinFailed) {
assert!(matches!(
e,
WatcherError::Fatal(TelemetryError::InvalidLogFile(_, _))
));
}
}
#[test]
fn chdir_failed() {
setup_fuelup_home();
struct ChdirFailed;
impl DaemoniseHelpers for ChdirFailed {
fn chdir(&self, _path: &Path) -> nix::Result<()> {
Err(Errno::EOWNERDEAD)
}
fn setup_stdio(
&self,
_log_filename: &str,
) -> std::result::Result<(), TelemetryError> {
Ok(())
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut ChdirFailed) {
assert_eq!(
e,
WatcherError::Fatal(TelemetryError::Nix(Errno::EOWNERDEAD.to_string()))
);
}
}
#[test]
fn sysconf_failed() {
setup_fuelup_home();
struct SysconfFailed;
impl DaemoniseHelpers for SysconfFailed {
fn sysconf(&self, _var: SysconfVar) -> nix::Result<Option<c_long>> {
Err(Errno::EOWNERDEAD)
}
fn setup_stdio(
&self,
_log_filename: &str,
) -> std::result::Result<(), TelemetryError> {
Ok(())
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut SysconfFailed) {
assert_eq!(
e,
WatcherError::Fatal(TelemetryError::Nix(Errno::EOWNERDEAD.to_string()))
);
}
}
#[test]
fn close_failed_with_ebadf() {
setup_fuelup_home();
struct CloseFailed;
impl DaemoniseHelpers for CloseFailed {
fn close(&self, _fd: c_int) -> nix::Result<()> {
Err(Errno::EBADF)
}
fn setup_stdio(
&self,
_log_filename: &str,
) -> std::result::Result<(), TelemetryError> {
Ok(())
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut CloseFailed) {
assert_eq!(
e,
WatcherError::Fatal(TelemetryError::Nix(Errno::EBADF.to_string()))
);
}
}
#[test]
fn close_failed_with_other_error() {
setup_fuelup_home();
struct CloseFailed;
impl DaemoniseHelpers for CloseFailed {
fn close(&self, _fd: c_int) -> nix::Result<()> {
Err(Errno::EOWNERDEAD)
}
fn setup_stdio(
&self,
_log_filename: &str,
) -> std::result::Result<(), TelemetryError> {
Ok(())
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
if let Err(e) = daemonise_with_helpers(&PathBuf::from("test.log"), &mut CloseFailed) {
assert_eq!(
e,
WatcherError::Fatal(TelemetryError::Nix(Errno::EOWNERDEAD.to_string()))
);
}
}
#[test]
fn ok() {
setup_fuelup_home();
struct AOk;
impl DaemoniseHelpers for AOk {
fn setup_stdio(
&self,
_log_filename: &str,
) -> std::result::Result<(), TelemetryError> {
Ok(())
}
fn fork(&mut self) -> nix::Result<ForkResult> {
let original_parent = getpid();
match unsafe { fork() }.unwrap() {
ForkResult::Parent { child: _child } => Ok(ForkResult::Child),
ForkResult::Child => Ok(ForkResult::Parent {
child: original_parent,
}),
}
}
}
let parent_pid = getpid();
let result = daemonise_with_helpers(&PathBuf::from("test.log"), &mut AOk);
if getpid() == parent_pid {
assert_eq!(result, Ok(None));
}
}
}
}
#[cfg(test)]
mod setup_stdio {
use super::*;
use rusty_fork::rusty_fork_test;
rusty_fork_test! {
#[test]
fn create_append_failed() {
setup_fuelup_home();
struct CreateAppendFailed;
impl SetupStdioHelpers for CreateAppendFailed {
fn create_append(
&self,
_log_filename: &str,
) -> std::result::Result<File, std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Error creating append",
))
}
}
let result = setup_stdio_with_helpers(
&format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
&mut CreateAppendFailed,
);
assert!(matches!(result, Err(TelemetryError::IO(_))));
}
#[test]
fn first_dup2_failed() {
setup_fuelup_home();
struct FirstDup2Failed;
impl SetupStdioHelpers for FirstDup2Failed {
fn dup2(
&mut self,
_fd: c_int,
fd2: c_int,
) -> std::result::Result<c_int, nix::errno::Errno> {
assert_eq!(fd2, 2);
Err(nix::errno::Errno::EOWNERDEAD)
}
}
let result = setup_stdio_with_helpers(
&format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
&mut FirstDup2Failed,
);
assert!(matches!(result, Err(TelemetryError::Nix(_))));
}
#[test]
fn read_write_failed() {
setup_fuelup_home();
struct ReadWriteFailed;
impl SetupStdioHelpers for ReadWriteFailed {
fn read_write(&self, _path: &str) -> std::result::Result<File, std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Error reading write",
))
}
}
let result = setup_stdio_with_helpers(
&format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
&mut ReadWriteFailed,
);
assert!(matches!(result, Err(TelemetryError::IO(_))));
}
#[test]
fn second_dup2_failed() {
setup_fuelup_home();
#[derive(Default)]
struct SecondDup2Failed {
call_counter: usize,
}
impl SetupStdioHelpers for SecondDup2Failed {
fn dup2(
&mut self,
_fd: c_int,
fd2: c_int,
) -> std::result::Result<c_int, nix::errno::Errno> {
self.call_counter += 1;
if self.call_counter == 2 {
assert_eq!(fd2, 0);
Err(nix::errno::Errno::EOWNERDEAD)
} else {
Ok(0)
}
}
}
let result = setup_stdio_with_helpers(
&format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
&mut SecondDup2Failed::default(),
);
assert!(matches!(result, Err(TelemetryError::Nix(_))));
}
#[test]
fn third_dup2_failed() {
setup_fuelup_home();
#[derive(Default)]
struct ThirdDup2Failed {
call_counter: usize,
}
impl SetupStdioHelpers for ThirdDup2Failed {
fn dup2(
&mut self,
_fd: c_int,
fd2: c_int,
) -> std::result::Result<c_int, nix::errno::Errno> {
self.call_counter += 1;
if self.call_counter == 3 {
assert_eq!(fd2, 1);
Err(nix::errno::Errno::EOWNERDEAD)
} else {
Ok(0)
}
}
}
let result = setup_stdio_with_helpers(
&format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
&mut ThirdDup2Failed::default(),
);
assert!(matches!(result, Err(TelemetryError::Nix(_))));
}
#[test]
fn ok() {
setup_fuelup_home();
let result = setup_stdio_with_helpers(
&format!("{}/test.log", telemetry_config().unwrap().fuelup_log),
&mut DefaultSetupStdioHelpers,
);
assert!(matches!(result, Ok(())));
}
}
}