// Copyright 2021-2022 Ian Jackson and contributors to Hippotat
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-Hippotat-OpenSSL-Exception
// There is NO WARRANTY.

use std::convert::TryInto;
use std::ffi::CStr;
use std::io::IoSlice;
use std::os::raw::{c_char, c_int};
use std::os::unix::io::RawFd;
use std::slice;
use std::str;
use std::thread::panicking;

use easy_ext::ext;

use nix::errno::*;
use nix::fcntl::*;
use nix::unistd::*;
use nix::sys::stat::*;
use nix::sys::signal::*;
use nix::sys::uio::*;
use nix::sys::wait::*;

use hippotat::prelude as prelude;
use prelude::default;

pub struct Daemoniser {
  drop_bomb: Option<()>,
  intermediate_pid: Pid,
  null_fd: RawFd,
  st_wfd: RawFd,
}

fn crashv(ms: &[IoSlice<'_>]) -> ! {
  unsafe {
    let _ = writev(2, ms);
    libc::_exit(18);
  }
}
  
macro_rules! crashv { { $( $m:expr ),* $(,)? } => {
  match [
    "hippotatd: ",
    $( $m, )*
    "\n",
  ] {
    ms => {
      let ms = ms.map(|m| IoSlice::new(m.as_bytes()));
      crashv(&ms)
    }
  }
} }

macro_rules! cstr { { $b:tt } => {
  CStr::from_bytes_with_nul($b)
    .unwrap_or_else(|_| crashm("cstr not nul terminated?! bug!"))
} }

fn crashm(m: &str) -> ! {
  crashv!(m)
}
fn crashe(m: &str, e: Errno) -> ! {
  crashv!(m, ": ", e.desc())
}

#[ext]
impl<T> nix::Result<T> {
  fn context(self, m: &str) -> T {
    match self {
      Ok(y) => y,
      Err(e) => crashe(m, e),
    }
  }
}

const ITOA_BUFL: usize = 12;
fn c_itoa(value: c_int, buf: &mut [u8; ITOA_BUFL]) -> &str {
  unsafe {
    *buf = [b'.'; ITOA_BUFL];
    libc::snprintf({ let buf: *mut u8 = buf.as_mut_ptr(); buf as *mut c_char },
                   ITOA_BUFL-2,
                   cstr!(b"%x\0").as_ptr(),
                   value);
  }
  let s = buf.splitn(2, |&c| c == b'\0').next()
    .unwrap_or_else(|| crashm("splitn no next"));
  str::from_utf8(s).unwrap_or_else(|_| crashm("non-utf-8 from snprintf!"))
}

unsafe fn mdup2(oldfd: RawFd, newfd: RawFd, what: &str) {
  match dup2(oldfd, newfd) {
    Ok(got) if got == newfd => { },
    Ok(_) => crashm("dup2 gave wrong return value"),
    Err(e) => crashv!("dup2 ", what, ": ", e.desc()),
  }
}

unsafe fn write_status(st_wfd: RawFd, estatus: u8) {
  match write(st_wfd, slice::from_ref(&estatus)) {
    Ok(1) => {}
    Ok(_) => crashm("write child startup exit status: short write"),
    Err(e) => crashe("write child startup exit status", e),
  }
}

unsafe fn parent(st_rfd: RawFd) -> ! {
  let mut exitstatus = 0u8;
  loop {
    match read(st_rfd, slice::from_mut(&mut exitstatus)) {
      Ok(0) => crashm("startup/daemonisation failed"),
      Ok(1) => libc::_exit(exitstatus.into()),
      Ok(_) => crashm("read startup: excess read!"),
      Err(e) if e == Errno::EINTR => continue,
      Err(e) => crashe("read startup signal pipe", e),
    }
  }
}

unsafe fn intermediate(child: Pid, st_wfd: RawFd) -> ! {
  let mut wstatus: c_int = 0;

  let r = libc::waitpid(child.as_raw(), &mut wstatus, 0);
  if r == -1 { crashe("await child startup status",
                      nix::errno::from_i32(errno())) }
  if r != child.as_raw() { crashm("await child startup status: wrong pid") }

  let cooked = WaitStatus::from_raw(child, wstatus)
    .context("await child startup status: convert wait status");
  match cooked {
    WaitStatus::Exited(_, estatus) => {
      let estatus: u8 = estatus.try_into()
        .unwrap_or_else(|_| crashm(
          "await child startup status: exit status out of range!"));
      write_status(st_wfd, estatus);
      libc::_exit(0);
    }

    WaitStatus::Signaled(_, signal, coredump) => {
      crashv!("startup failed: died due to signal: ", signal.as_str(),
              if coredump { " (core dumped)" } else { "" });
    },

    _ => {
      crashv!("child startup exit status was strange!  0x",
              c_itoa(wstatus, &mut default()))
    }
  }
}

impl Daemoniser {
  /// Start daemonising - call before any threads created!
  pub fn phase1() -> Self {
    unsafe {
      let null_fd = open(cstr!(b"/dev/null\0"), OFlag::O_RDWR, Mode::empty())
        .context("open /dev/null");
      mdup2(null_fd, 0, "null onto stdin");

      let (st_rfd, st_wfd) = pipe().context("pipe");

      match fork().context("fork (1)") {
        ForkResult::Child => { }
        ForkResult::Parent { child: _ } => {
          close(st_wfd).context("close st_wfd pipe");
          parent(st_rfd)
        },
      }

      close(st_rfd).context("close st_rfd pipe");
      setsid().context("setsid");
      let intermediate_pid = Pid::this();

      match fork().context("fork (2)") {
        ForkResult::Child => { }
        ForkResult::Parent { child } => {
          intermediate(child, st_wfd)
        },
      }

      Daemoniser {
        drop_bomb: Some(()),
        intermediate_pid,
        null_fd,
        st_wfd,
      }
    }
  }

  pub fn complete(mut self) {
    unsafe {
      mdup2(self.null_fd, 1, "null over stdin");

      if Pid::parent() != self.intermediate_pid {
        crashm(
          "startup complete, but our parent is no longer the intermediate?");
      }
      kill(self.intermediate_pid, Some(Signal::SIGKILL))
        .context("kill intermediate (after startup complete)");

      write_status(self.st_wfd, 0);
      mdup2(self.null_fd, 2, "null over stderrr");

      self.drop_bomb.take();
    }
  }
}

impl Drop for Daemoniser {
  fn drop(&mut self) {
    if let Some(()) = self.drop_bomb.take() {
      if panicking() {
        // We will crash in due course, having printed some messages
        // to stderr, presumably.
        return
      } else {
        panic!("Daemonizer object dropped unexpectedly, startup failed");
      }
    }
  }
}
