use std::fs::File;
use std::io;
#[cfg(unix)]
use std::os::unix::fs::DirBuilderExt as _;
#[cfg(unix)]
use std::os::unix::io::RawFd;
#[cfg(unix)]
use std::os::unix::process::CommandExt as _;
use std::path::Path;
use std::process::{Child, Command, Stdio};
#[cfg(unix)]
use nix::fcntl::{FcntlArg, FdFlag, fcntl};
use shep_core::paths::ShepPaths;
pub const DAEMON_STDOUT_LOG: &str = "shepd.out.log";
pub const DAEMON_STDERR_LOG: &str = "shepd.err.log";
pub fn launch_command(paths: &ShepPaths) -> io::Result<Command> {
let mut log_dir = std::fs::DirBuilder::new();
log_dir.recursive(true);
#[cfg(unix)]
log_dir.mode(shep_daemon::boot::DIR_MODE);
log_dir.create(&paths.logs)?;
let mut cmd = Command::new(std::env::current_exe()?);
cmd.arg("daemon")
.env("SHEP_HOME", &paths.home)
.stdout(emptied_appending(&paths.logs.join(DAEMON_STDOUT_LOG))?)
.stderr(emptied_appending(&paths.logs.join(DAEMON_STDERR_LOG))?)
.stdin(Stdio::null());
#[cfg(unix)]
cmd.process_group(0);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt as _;
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB);
}
Ok(cmd)
}
fn emptied_appending(path: &Path) -> io::Result<File> {
#[cfg(windows)]
File::create(path)?;
let file = File::options().create(true).append(true).open(path)?;
#[cfg(unix)]
file.set_len(0)?;
Ok(file)
}
#[cfg(unix)]
const FIRST_NON_STDIO_FD: RawFd = 3;
#[cfg_attr(windows, allow(dead_code))]
const FD_DIR: &str = "/dev/fd";
#[cfg(unix)]
fn seal_inherited_fds() {
let Ok(entries) = std::fs::read_dir(FD_DIR) else {
return;
};
for entry in entries.flatten() {
let Some(fd) = entry
.file_name()
.to_str()
.and_then(|name| name.parse::<RawFd>().ok())
else {
continue;
};
if fd < FIRST_NON_STDIO_FD {
continue;
}
let _ = fcntl(fd, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC));
}
}
#[cfg(unix)]
pub fn launch_daemon(paths: &ShepPaths) -> io::Result<Child> {
seal_inherited_fds();
launch_command(paths)?.spawn()
}
#[cfg(windows)]
const DETACHED_PROCESS: u32 = 0x0000_0008;
#[cfg(windows)]
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
#[cfg(windows)]
const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;
#[cfg(windows)]
pub fn launch_daemon(paths: &ShepPaths) -> io::Result<Child> {
use std::os::windows::process::CommandExt as _;
shep_daemon::sys_windows::seal_std_handles();
match launch_command(paths)?.spawn() {
Ok(child) => Ok(child),
Err(err) if err.raw_os_error() == Some(5) => {
let mut cmd = launch_command(paths)?;
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
cmd.spawn()
}
Err(err) => Err(err),
}
}
#[cfg(all(test, unix))]
mod tests {
use std::os::unix::fs::PermissionsExt as _;
use std::path::Path;
use super::*;
fn test_paths(dir: &tempfile::TempDir) -> ShepPaths {
ShepPaths::resolve(
&|k| (k == "SHEP_HOME").then(|| dir.path().to_string_lossy().into_owned()),
Path::new("/nonexistent"),
)
}
fn mode_of(path: &Path) -> u32 {
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[test]
fn the_launcher_never_sets_the_readiness_fd_variable() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
let cmd = launch_command(&paths).unwrap(); assert!(
!cmd.get_envs().any(|(k, _)| k == "SHEP_READY_FD"),
"the whole phase design rests on readiness being a handshake, not an fd"
);
}
#[test]
fn the_launcher_pins_shep_home_to_the_resolved_path() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
let cmd = launch_command(&paths).unwrap();
let home = cmd
.get_envs()
.find(|(k, _)| *k == "SHEP_HOME")
.and_then(|(_, v)| v)
.expect("the child must not re-resolve $SHEP_HOME from ambient environment");
assert_eq!(Path::new(home), paths.home);
}
#[test]
fn the_launcher_runs_this_binarys_hidden_daemon_subcommand() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
let cmd = launch_command(&paths).unwrap();
assert_eq!(
cmd.get_program(),
std::env::current_exe().unwrap().as_os_str()
);
let args: Vec<_> = cmd.get_args().collect();
assert_eq!(args, ["daemon"]);
}
#[test]
fn the_launcher_creates_the_log_directory_before_spawning() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
assert!(!paths.logs.exists(), "precondition: a cold $SHEP_HOME");
let _cmd = launch_command(&paths).unwrap();
assert!(paths.logs.is_dir(), "the redirect targets must be openable");
assert_eq!(mode_of(&paths.logs), shep_daemon::boot::DIR_MODE);
}
#[test]
fn the_daemons_own_log_survives_a_truncation_without_a_hole() {
use std::io::Write as _;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("shepd.err.log");
let mut inherited = emptied_appending(&path).unwrap();
inherited.write_all(b"aaaaaaaaaa").unwrap();
inherited.flush().unwrap();
File::options()
.write(true)
.truncate(true)
.open(&path)
.unwrap();
inherited.write_all(b"bbb").unwrap();
inherited.flush().unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
3,
"a descriptor keeping its own offset would leave 13 bytes here, the first ten of \
them NUL"
);
}
#[test]
fn relaunching_still_starts_the_daemons_own_logs_empty() {
let dir = tempfile::tempdir().unwrap();
let paths = test_paths(&dir);
std::fs::create_dir_all(&paths.logs).unwrap();
let path = paths.logs.join(DAEMON_STDOUT_LOG);
std::fs::write(&path, b"a previous daemon's output").unwrap();
let _cmd = launch_command(&paths).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
#[cfg(unix)]
fn is_close_on_exec(fd: RawFd) -> bool {
let flags = fcntl(fd, FcntlArg::F_GETFD).unwrap();
FdFlag::from_bits_truncate(flags).contains(FdFlag::FD_CLOEXEC)
}
#[test]
fn the_sweep_marks_an_inherited_descriptor_close_on_exec() {
let (leaked, _peer) = std::os::unix::net::UnixStream::pair().unwrap();
let fd = std::os::unix::io::AsRawFd::as_raw_fd(&leaked);
fcntl(fd, FcntlArg::F_SETFD(FdFlag::empty())).unwrap();
assert!(!is_close_on_exec(fd), "the fixture must start inheritable");
seal_inherited_fds();
assert!(
is_close_on_exec(fd),
"fd {fd} would cross the exec and be held for the daemon's whole life"
);
}
#[test]
fn the_sweep_leaves_stdio_alone() {
assert!(
!is_close_on_exec(0),
"fixture: this process's own stdin is inherited and inheritable"
);
seal_inherited_fds();
assert!(!is_close_on_exec(0), "the sweep must start above stdio");
}
}