signal handling / graceful shutdown
It'd be nice to have a simple way of handling SIGINT / SIGTERM for gracefully shutting down stuff
One use case in particular to highlight would be testing.
I just ran into this and worked around the lack of a stop() function by running the executable with std::process:Child. I was relatively certain I might have been able to use serve() in a thread and maybe set up a channel to listen for some shutdown message (you can't just kill pthreads), but making a child process and managing killing it was much easier.
The code isn't the greatest but if anyone runs into this:
/// Setup API server for use in testing
pub fn setup_api_server_process() -> Result<APIServerTestInfo, Error> {
// Generate configuration for the server
let cfg = AppConfig::new_randomized()?;
let cfg_toml_contents = toml::to_string(&cfg).expect("failed to config to TOML");
// Write config to temp file
let dir = tempdir().unwrap();
let cfg_path = String::from(dir.path().join("cfg.toml").to_str().unwrap());
let mut file: File = File::create(&cfg_path).unwrap();
write!(file, "{}", cfg_toml_contents).expect("Failed to write temporary file");
// Spawn a child process (that never returns) for the API server
let child = Command::new("cargo")
.arg("run")
.arg("--")
.arg("-c")
.arg(cfg_path)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn child thread for test API server");
// Wait for the child process something
// FUTURE: this is fragile, would be better to wait for some sort of "server running" message on stdout
thread::sleep(time::Duration::from_millis(STARTUP_WAIT));
Ok((cfg, child))
}
I found a way to make the child process a tiny bit less fragile by waiting on output of a very specific message on stdout:
// It is surprisingly hard to redirect output from a child process:
// https://stackoverflow.com/questions/42726095/how-to-implement-redirection-of-stdout-of-a-child-process-to-a-file
// https://stackoverflow.com/questions/43949612/redirect-output-of-child-process-spawned-from-rust
let output_path = String::from(dir.path().join("output.log").to_str().unwrap());
let output = File::create(output_path.clone()).unwrap().into_raw_fd();
let raw_output_fd = unsafe {Stdio::from_raw_fd(output)};
// Spawn a child process (that never returns) for the API server
let child = Command::new("cargo")
.arg("run")
.arg("--")
.arg("-c")
.arg(cfg_path)
.stdout(raw_output_fd)
.spawn()
.expect("failed to spawn child thread for test API server");
// Read from the output file to which child process stdout is going, as the child process writes it
let mut output_file = File::open(output_path).unwrap();
let mut log_contents = String::new();
// Wait until the server says it's ready
loop {
output_file.read_to_string(&mut log_contents)?;
if log_contents.contains(SERVER_STARTED_MESSAGE) {
thread::sleep(time::Duration::from_millis(STARTUP_EXTRA_WAIT));
break;
}
}
Still not particularly proud of it, but it does work...