use std::io::{self, BufRead, BufReader, ErrorKind};
use std::os::unix::net::UnixStream;
use std::os::unix::prelude::OwnedFd;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::Duration;
use crate::daemon::instance;
use crate::{Error, Result};
const EOF: usize = 0;
#[derive(Debug)]
pub enum Start {
Started {
pid: u32,
},
AlreadyRunning,
}
pub(super) fn launch(mut command: Command, dir: &Path, timeout: Duration) -> Result<Start> {
close_fds::set_fds_cloexec_threadsafe(3, &[]);
let (parent_stream, child_stream) = UnixStream::pair().map_err(Error::DaemonLaunch)?;
let child = command
.stdout(OwnedFd::from(child_stream))
.stdin(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.map_err(Error::DaemonLaunch)?;
drop(command);
parent_stream
.set_read_timeout(Some(timeout))
.map_err(Error::DaemonLaunch)?;
let mut line = String::new();
let result = BufReader::new(&parent_stream).read_line(&mut line);
match result {
Ok(EOF) => explain_early_exit(child, dir, timeout),
Ok(_) => Ok(leave_running(child)),
Err(e) => Err(kill_unready(child, e)),
}
}
fn explain_early_exit(child: Child, dir: &Path, timeout: Duration) -> Result<Start> {
let output = child.wait_with_output().map_err(Error::DaemonLaunch)?;
if !instance::lock_held(dir)? {
return Err(Error::DaemonStartFailed {
status: output.status,
stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
});
}
if wait_for_socket(&instance::socket_path(dir), timeout) {
Ok(Start::AlreadyRunning)
} else {
Err(Error::DaemonUnresponsive)
}
}
fn leave_running(mut child: Child) -> Start {
let pid = child.id();
thread::spawn(move || {
drop(child.wait());
});
Start::Started { pid }
}
fn kill_unready(mut child: Child, error: io::Error) -> Error {
drop(child.kill());
drop(child.wait());
if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) {
Error::DaemonStartTimeout
} else {
Error::DaemonLaunch(error)
}
}
fn wait_for_socket(path: &Path, timeout: Duration) -> bool {
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if instance::connect(path).is_ok() {
return true;
}
std::thread::sleep(Duration::from_millis(10));
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::instance;
use std::io::Read;
use std::os::unix::net::UnixListener;
use std::process::Command;
use std::sync::mpsc;
#[test]
fn reads_the_ready_line() {
let dir = tempfile::tempdir().unwrap();
let result = launch(sh("echo listening"), dir.path(), Duration::from_secs(5));
assert!(matches!(result, Ok(Start::Started { .. })));
}
#[test]
fn reports_a_child_that_exits_early() {
let dir = tempfile::tempdir().unwrap();
let result = launch(
sh("echo boom >&2; exit 3"),
dir.path(),
Duration::from_secs(5),
);
assert!(
matches!(result, Err(Error::DaemonStartFailed { status, stderr }) if status.code() == Some(3) && stderr == "boom")
);
}
#[test]
fn reports_an_early_exit_in_a_missing_state_directory() {
let root = tempfile::tempdir().unwrap();
let dir = root.path().join("state");
let result = launch(sh("echo boom >&2; exit 3"), &dir, Duration::from_secs(5));
assert!(matches!(result, Err(Error::DaemonStartFailed { .. })));
assert!(!dir.exists());
}
#[test]
fn kills_a_child_that_never_answers() {
let dir = tempfile::tempdir().unwrap();
let result = launch(sh("sleep 10"), dir.path(), Duration::from_millis(100));
assert!(matches!(result, Err(Error::DaemonStartTimeout)));
}
#[test]
fn reports_a_running_daemon() {
let dir = tempfile::tempdir().unwrap();
let _lock = instance::lock(dir.path()).unwrap();
let socket_path = instance::socket_path(dir.path());
let _listener = UnixListener::bind(socket_path).unwrap();
let result = launch(sh("exit 1"), dir.path(), Duration::from_secs(5));
assert!(matches!(result, Ok(Start::AlreadyRunning)));
}
#[test]
fn reports_an_unresponsive_daemon() {
let dir = tempfile::tempdir().unwrap();
let _lock = instance::lock(dir.path()).unwrap();
let result = launch(sh("exit 1"), dir.path(), Duration::from_millis(100));
assert!(matches!(result, Err(Error::DaemonUnresponsive)));
}
#[test]
fn does_not_pass_inherited_descriptors() {
let dir = tempfile::tempdir().unwrap();
let (mut reader, writer) = std::io::pipe().unwrap();
rustix::io::fcntl_setfd(&writer, rustix::io::FdFlags::empty()).unwrap();
let result = launch(
sh("echo listening; sleep 3"),
dir.path(),
Duration::from_secs(5),
);
assert!(matches!(result, Ok(Start::Started { .. })));
drop(writer);
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || tx.send(reader.read_to_end(&mut Vec::new())));
let read = rx
.recv_timeout(Duration::from_secs(1))
.expect("the child kept the inherited pipe open");
assert_eq!(read.unwrap(), 0);
}
#[test]
fn reaps_the_daemon_after_it_exits() {
let dir = tempfile::tempdir().unwrap();
let Ok(Start::Started { pid }) =
launch(sh("echo listening"), dir.path(), Duration::from_secs(5))
else {
panic!("the child did not start");
};
let pid = rustix::process::Pid::from_raw(i32::try_from(pid).unwrap()).unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
let mut result = rustix::process::test_kill_process(pid);
while result.is_ok() && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
result = rustix::process::test_kill_process(pid);
}
assert_eq!(result, Err(rustix::io::Errno::SRCH));
}
fn sh(script: &str) -> Command {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(script);
cmd
}
}