1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
|
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
}
}
}
|