use anyhow::Result;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
const MAX_ERROR_MSG_LEN: usize = 4096;
pub fn daemonize<F, R>(daemon_fn: F, ready_check: R, timeout: Duration) -> Result<()>
where
F: FnOnce() -> Result<()> + Send + 'static,
R: Fn() -> bool,
{
let mut pipe_fds: [libc::c_int; 2] = [0; 2];
if unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } != 0 {
anyhow::bail!("Failed to create pipe: {}", std::io::Error::last_os_error());
}
let (read_fd, write_fd) = (pipe_fds[0], pipe_fds[1]);
match unsafe { libc::fork() } {
-1 => {
unsafe {
libc::close(read_fd);
libc::close(write_fd);
}
anyhow::bail!("Fork failed: {}", std::io::Error::last_os_error());
}
0 => {
unsafe { libc::close(read_fd) };
if unsafe { libc::setsid() } == -1 {
let _ = signal_parent(write_fd, Err("Failed to create new session".to_string()));
std::process::exit(1);
}
let (daemon_thread, error_msg) = start_daemon(daemon_fn);
let start = std::time::Instant::now();
let ready = loop {
if ready_check() {
break true;
}
if daemon_thread.is_finished() {
break false;
}
if start.elapsed() >= timeout {
break false;
}
std::thread::sleep(Duration::from_millis(50));
};
let signal_result = if ready {
Ok(())
} else {
let err_msg = error_msg
.lock()
.ok()
.and_then(|guard| guard.clone())
.unwrap_or_else(|| "Daemon failed to start".to_string());
Err(err_msg)
};
let _ = signal_parent(write_fd, signal_result);
unsafe { libc::close(write_fd) };
if !ready {
std::process::exit(1);
}
match daemon_thread.join() {
Ok(Ok(())) => std::process::exit(0),
_ => std::process::exit(1),
}
}
_child_pid => {
unsafe { libc::close(write_fd) };
let result = wait_for_signal(read_fd);
unsafe { libc::close(read_fd) };
match result {
Ok(()) => Ok(()),
Err(msg) => anyhow::bail!("{}", msg),
}
}
}
}
fn signal_parent(fd: libc::c_int, result: Result<(), String>) -> Result<()> {
let buf = match &result {
Ok(()) => vec![0u8],
Err(msg) => {
let msg_bytes = msg.as_bytes();
let len = msg_bytes.len().min(MAX_ERROR_MSG_LEN);
let mut buf = Vec::with_capacity(1 + 4 + len);
buf.push(1u8);
buf.extend_from_slice(&(len as u32).to_be_bytes());
buf.extend_from_slice(&msg_bytes[..len]);
buf
}
};
let mut written_total = 0;
while written_total < buf.len() {
let written = unsafe {
libc::write(
fd,
buf[written_total..].as_ptr() as *const libc::c_void,
buf.len() - written_total,
)
};
if written > 0 {
written_total += written as usize;
} else if written == -1 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
anyhow::bail!("Failed to signal parent: {}", err);
} else {
anyhow::bail!("Unexpected write result: {}", written);
}
}
Ok(())
}
fn wait_for_signal(fd: libc::c_int) -> Result<(), String> {
let status = match read_exact(fd, 1) {
Some(buf) => buf[0],
None => return Err("Daemon process terminated unexpectedly".to_string()),
};
if status == 0 {
return Ok(());
}
let len_bytes = match read_exact(fd, 4) {
Some(buf) => buf,
None => return Err("Daemon failed to start".to_string()),
};
let len = u32::from_be_bytes(len_bytes.try_into().unwrap()) as usize;
let len = len.min(MAX_ERROR_MSG_LEN);
if len == 0 {
return Err("Daemon failed to start".to_string());
}
match read_exact(fd, len) {
Some(buf) => Err(String::from_utf8_lossy(&buf).into_owned()),
None => Err("Daemon failed to start".to_string()),
}
}
fn read_exact(fd: libc::c_int, n: usize) -> Option<Vec<u8>> {
let mut buf = vec![0u8; n];
let mut read_total = 0;
while read_total < n {
let result = unsafe {
libc::read(
fd,
buf[read_total..].as_mut_ptr() as *mut libc::c_void,
n - read_total,
)
};
if result > 0 {
read_total += result as usize;
} else if result == 0 {
return None;
} else {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
return None;
}
}
Some(buf)
}
fn start_daemon<F>(
daemon_fn: F,
) -> (
std::thread::JoinHandle<Result<()>>,
Arc<Mutex<Option<String>>>,
)
where
F: FnOnce() -> Result<()> + Send + 'static,
{
redirect_stdio();
let error_msg: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let daemon_thread = {
let error_msg = error_msg.clone();
std::thread::spawn(move || {
let result = daemon_fn();
if let Err(ref e) = result {
if let Ok(mut guard) = error_msg.lock() {
*guard = Some(format!("{:#}", e));
}
}
result
})
};
(daemon_thread, error_msg)
}
fn redirect_stdio() {
unsafe {
let devnull = libc::open(c"/dev/null".as_ptr(), libc::O_RDWR);
if devnull >= 0 {
libc::dup2(devnull, 0);
libc::dup2(devnull, 1);
libc::dup2(devnull, 2);
if devnull > 2 {
libc::close(devnull);
}
}
}
}