mod test_utils; use std::{ env, fs::{create_dir_all, remove_file, OpenOptions}, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}, }; const CTRL_INDEX: &str = "CTRL_INDEX"; const CRASHFILE: &str = "CRASHFILE"; const RUNS: usize = 3; const MILLIS: u64 = 50; // use the same technique as test_utils::dispatch to launch itself in child mode, // but do it twice: // controller starts parent, parent starts child // controller keeps running and verifies that the child's panic file is created (or not), // parent terminates directly and thus destroys the stderr of child, thus forcing child to panic #[test] fn main() { match env::var(CTRL_INDEX).as_ref() { Err(_) => { controller(); } Ok(s) if s == "parent" => { parent(false); } Ok(s) if s == "parent_panic" => { parent(true); } Ok(s) if s == "child" => { child(false); } Ok(s) if s == "child_panic" => { child(true); } Ok(s) => panic!("Unexpected value {s}"), } } fn controller() { let progpath = env::args().next().unwrap(); create_dir_all(crashdump_file().parent().unwrap()).unwrap(); remove_file(crashdump_file()).ok(); // First run: don't panic let mut child = Command::new(progpath.clone()) .env(CTRL_INDEX, "parent") .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() .unwrap(); assert!(child.wait().expect("failed to wait on child").success()); // check that no crashdump_file was written std::thread::sleep(std::time::Duration::from_millis(200)); assert!(!Path::new(&crashdump_file()).try_exists().unwrap()); // Second run: panic let mut child = Command::new(progpath) .env(CTRL_INDEX, "parent_panic") .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() .unwrap(); assert!(child.wait().expect("failed to wait on child").success()); // check that crashdump_file was written std::thread::sleep(std::time::Duration::from_millis(200)); assert!(Path::new(&crashdump_file()).try_exists().unwrap()); } fn parent(panic: bool) { let progpath = std::env::args().next().unwrap(); // we don't want to wait here, and it's not an issue because this is not a long running program #[allow(clippy::zombie_processes)] // spawn child and terminate directly, thus destroying the child's stderr Command::new(progpath) .env(CTRL_INDEX, if panic { "child_panic" } else { "child" }) .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() .unwrap(); } fn child(panic: bool) { let original_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic| { let backtrace = std::backtrace::Backtrace::capture(); let mut file = OpenOptions::new() .create(true) .write(true) .truncate(true) .open(crashdump_file()) .unwrap(); file.write_all(format!("Panic occured:\n{}\n{}\n", panic, backtrace).as_bytes()) .unwrap(); file.flush().unwrap(); original_hook(panic); })); let _logger = flexi_logger::Logger::try_with_str("info") .unwrap() .log_to_stderr() .panic_if_error_channel_is_broken(panic) .start() .unwrap(); for i in 0..RUNS { log::info!("log test ({i})"); // <-- causes panic when parent terminated std::thread::sleep(std::time::Duration::from_millis(MILLIS)); } } // controller is first caller and writes name to env, all other calls should find the env // and take the value from there fn crashdump_file() -> PathBuf { match std::env::var(CRASHFILE) { Ok(s) => Path::new(&s).to_path_buf(), Err(_) => { let progname = PathBuf::from(std::env::args().next().unwrap()) .file_name() .unwrap() .to_string_lossy() .to_string(); let path = test_utils::file(&format!("./{progname}.log")); std::env::set_var(CRASHFILE, &path); path } } }