use std::io::{self, Write};
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use crate::error::DaemonizeError;
pub(crate) struct NotifyPipe(OwnedFd);
impl NotifyPipe {
pub(crate) fn new(fd: OwnedFd) -> Self {
NotifyPipe(fd)
}
pub(crate) fn as_fd(&self) -> BorrowedFd<'_> {
self.0.as_fd()
}
#[must_use = "the parent process blocks until notified; ignoring this Result may leave it waiting"]
pub(crate) fn signal_ready(self) -> io::Result<()> {
self.write_all(&SUCCESS)
}
pub(crate) fn signal_error(self, err: &DaemonizeError) {
let _ = self.write_all(&error_bytes(err));
}
pub(crate) fn signal_unnotified(self) {
let _ = self.write_all(&unnotified_bytes());
}
fn write_all(self, bytes: &[u8]) -> io::Result<()> {
let mut file = io::BufWriter::new(std::fs::File::from(self.0));
file.write_all(bytes)?;
file.flush()
}
}
const UNNOTIFIED_CODE: u8 = 1;
const UNNOTIFIED_MSG: &[u8] = b"daemon exited without signaling readiness";
pub(crate) const SUCCESS: [u8; 1] = [0x00];
pub(crate) fn error_bytes(err: &DaemonizeError) -> Vec<u8> {
encode(err.exit_code(), err.to_string().as_bytes())
}
pub(crate) fn unnotified_bytes() -> Vec<u8> {
encode(UNNOTIFIED_CODE, UNNOTIFIED_MSG)
}
fn encode(code: u8, msg: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(1 + msg.len());
buf.push(code);
buf.extend_from_slice(msg);
buf
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Outcome {
Success,
Failure { code: i32, message: String },
}
pub(crate) fn decode(buf: &[u8]) -> Outcome {
match buf.first() {
None | Some(&0x00) => Outcome::Success,
Some(&code) => Outcome::Failure {
code: code as i32,
message: String::from_utf8_lossy(&buf[1..]).into_owned(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
fn make_pipe() -> (OwnedFd, OwnedFd) {
nix::unistd::pipe().unwrap()
}
fn read_pipe(rd: OwnedFd) -> Vec<u8> {
let mut buf = Vec::new();
std::fs::File::from(rd).read_to_end(&mut buf).unwrap();
buf
}
#[test]
fn signal_ready_writes_success_byte() {
let (rd, wr) = make_pipe();
NotifyPipe::new(wr).signal_ready().unwrap();
assert_eq!(read_pipe(rd), SUCCESS);
}
#[test]
fn signal_error_writes_protocol() {
let (rd, wr) = make_pipe();
let err = DaemonizeError::ForkFailed("boom".into());
NotifyPipe::new(wr).signal_error(&err);
assert_eq!(decode(&read_pipe(rd)), decode(&error_bytes(&err)));
}
#[test]
fn signal_unnotified_writes_protocol() {
let (rd, wr) = make_pipe();
NotifyPipe::new(wr).signal_unnotified();
assert_eq!(
decode(&read_pipe(rd)),
Outcome::Failure {
code: UNNOTIFIED_CODE as i32,
message: "daemon exited without signaling readiness".into(),
}
);
}
#[test]
fn signal_ready_errors_on_closed_reader() {
let (rd, wr) = make_pipe();
drop(rd); let result = NotifyPipe::new(wr).signal_ready();
assert!(result.is_err());
}
#[test]
fn decode_success_byte() {
assert_eq!(decode(&SUCCESS), Outcome::Success);
}
#[test]
fn decode_eof_is_success() {
assert_eq!(decode(&[]), Outcome::Success);
}
#[test]
fn error_bytes_round_trip() {
let err = DaemonizeError::ForkFailed("boom".into());
let code = err.exit_code();
assert_eq!(
decode(&error_bytes(&err)),
Outcome::Failure {
code: code as i32,
message: "fork failed: boom".into(),
}
);
}
#[test]
fn application_zero_code_is_not_silently_success() {
let err = DaemonizeError::application(0, "boom");
match decode(&error_bytes(&err)) {
Outcome::Failure { code, message } => {
assert_ne!(code, 0);
assert_eq!(message, "application error: boom");
}
Outcome::Success => panic!("a reported error was decoded as success"),
}
}
#[test]
fn unnotified_round_trip() {
assert_eq!(
decode(&unnotified_bytes()),
Outcome::Failure {
code: UNNOTIFIED_CODE as i32,
message: "daemon exited without signaling readiness".into(),
}
);
}
}