use std::io::{Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
use std::path::Path;
use nix::sys::wait::waitpid;
use nix::unistd::{ForkResult, Pid, fork, setsid};
use super::DetachError;
const STARTUP_OK: u8 = 1;
const STARTUP_FAIL: u8 = 0;
pub fn detach(log_dir: &Path) -> Result<(), DetachError> {
let (reader, writer) = nix::unistd::pipe().map_err(syscall_error("pipe"))?;
match unsafe { fork() }.map_err(syscall_error("fork"))? {
ForkResult::Parent { child } => {
parent_after_first_fork(reader, writer, child)?;
unreachable!("parent exited above");
},
ForkResult::Child => {
child_after_first_fork(reader, writer, log_dir)?;
Ok(())
},
}
}
fn parent_after_first_fork(reader: OwnedFd, writer: OwnedFd, child: Pid) -> Result<(), DetachError> {
drop(writer);
let _ = waitpid(child, None);
let mut reader: std::fs::File = reader.into();
let mut first = [0u8; 1];
let n = reader.read(&mut first).map_err(io_error("read handshake"))?;
if n == 0 {
return Err(DetachError::ChildReportedFailure {
message: "child closed the handshake pipe without writing".to_string(),
});
}
match first[0] {
STARTUP_OK => {
std::process::exit(0);
},
STARTUP_FAIL => {
let mut rest = String::new();
let _ = reader.read_to_string(&mut rest);
Err(DetachError::ChildReportedFailure {
message: rest.trim().to_string(),
})
},
other => Err(DetachError::ChildReportedFailure {
message: format!("unrecognized handshake byte 0x{other:02x}"),
}),
}
}
fn child_after_first_fork(reader: OwnedFd, writer: OwnedFd, log_dir: &Path) -> Result<(), DetachError> {
drop(reader);
setsid().map_err(syscall_error("setsid"))?;
match unsafe { fork() }.map_err(syscall_error("fork2"))? {
ForkResult::Parent { .. } => {
drop(writer);
unsafe { libc::_exit(0) };
},
ForkResult::Child => grandchild_setup(writer, log_dir),
}
}
fn grandchild_setup(writer: OwnedFd, _log_dir: &Path) -> Result<(), DetachError> {
let writer_fd = writer.as_raw_fd();
if let Err(err) = ignore_sighup() {
report_failure_and_exit(writer_fd, &format!("{err}"));
}
if let Err(err) = redirect_pre_handshake_stdio() {
report_failure_and_exit(writer_fd, &format!("{err}"));
}
let mut w: std::fs::File = writer.into();
if let Err(err) = w.write_all(&[STARTUP_OK]) {
let _ = err;
}
drop(w);
if let Err(err) = redirect_stderr_to_devnull() {
let _ = err;
}
Ok(())
}
fn report_failure_and_exit(writer_fd: std::os::fd::RawFd, message: &str) -> ! {
let mut file = unsafe { std::fs::File::from_raw_fd(writer_fd) };
let _ = file.write_all(&[STARTUP_FAIL]);
let _ = file.write_all(message.as_bytes());
let _ = file.flush();
std::mem::forget(file);
unsafe { libc::_exit(1) };
}
fn redirect_pre_handshake_stdio() -> Result<(), DetachError> {
use std::fs::OpenOptions;
let devnull_in = OpenOptions::new()
.read(true)
.open("/dev/null")
.map_err(io_error("open /dev/null for stdin"))?;
dup2_raw(devnull_in.as_raw_fd(), libc::STDIN_FILENO, "dup2 stdin")?;
drop(devnull_in);
let devnull_out = OpenOptions::new()
.write(true)
.open("/dev/null")
.map_err(io_error("open /dev/null for stdout"))?;
dup2_raw(devnull_out.as_raw_fd(), libc::STDOUT_FILENO, "dup2 stdout")?;
drop(devnull_out);
Ok(())
}
fn redirect_stderr_to_devnull() -> Result<(), DetachError> {
use std::fs::OpenOptions;
let devnull_err = OpenOptions::new()
.write(true)
.open("/dev/null")
.map_err(io_error("open /dev/null for stderr"))?;
dup2_raw(devnull_err.as_raw_fd(), libc::STDERR_FILENO, "dup2 stderr")?;
drop(devnull_err);
Ok(())
}
fn ignore_sighup() -> Result<(), DetachError> {
let rc = unsafe { libc::signal(libc::SIGHUP, libc::SIG_IGN) };
if rc == libc::SIG_ERR {
return Err(DetachError::Syscall {
step: "signal SIGHUP",
source: std::io::Error::last_os_error(),
});
}
Ok(())
}
fn dup2_raw(old: std::os::fd::RawFd, new: std::os::fd::RawFd, step: &'static str) -> Result<(), DetachError> {
let rc = unsafe { libc::dup2(old, new) };
if rc == -1 {
return Err(DetachError::Syscall {
step,
source: std::io::Error::last_os_error(),
});
}
Ok(())
}
fn syscall_error(step: &'static str) -> impl FnOnce(nix::errno::Errno) -> DetachError {
move |errno| DetachError::Syscall {
step,
source: std::io::Error::from_raw_os_error(errno as i32),
}
}
fn io_error(step: &'static str) -> impl FnOnce(std::io::Error) -> DetachError {
move |source| DetachError::Syscall { step, source }
}
#[allow(dead_code)]
fn owned_to_raw(fd: OwnedFd) -> std::os::fd::RawFd {
fd.into_raw_fd()
}