//! Wait for a process to change status
use crate::errno::Errno;
use crate::sys::signal::Signal;
use crate::unistd::Pid;
use crate::Result;
use cfg_if::cfg_if;
use libc::{self, c_int};
use std::convert::TryFrom;
#[cfg(any(
    target_os = "android",
    all(target_os = "linux", not(target_env = "uclibc")),
))]
use std::os::unix::io::RawFd;

libc_bitflags!(
    /// Controls the behavior of [`waitpid`].
    pub struct WaitPidFlag: c_int {
        /// Do not block when there are no processes wishing to report status.
        WNOHANG;
        /// Report the status of selected processes which are stopped due to a
        /// [`SIGTTIN`](crate::sys::signal::Signal::SIGTTIN),
        /// [`SIGTTOU`](crate::sys::signal::Signal::SIGTTOU),
        /// [`SIGTSTP`](crate::sys::signal::Signal::SIGTSTP), or
        /// [`SIGSTOP`](crate::sys::signal::Signal::SIGSTOP) signal.
        WUNTRACED;
        /// Report the status of selected processes which have terminated.
        #[cfg(any(target_os = "android",
                  target_os = "freebsd",
                  target_os = "haiku",
                  target_os = "ios",
                  target_os = "linux",
                  target_os = "redox",
                  target_os = "macos",
                  target_os = "netbsd"))]
        #[cfg_attr(docsrs, doc(cfg(all())))]
        WEXITED;
        /// Report the status of selected processes that have continued from a
        /// job control stop by receiving a
        /// [`SIGCONT`](crate::sys::signal::Signal::SIGCONT) signal.
        WCONTINUED;
        /// An alias for WUNTRACED.
        #[cfg(any(target_os = "android",
                  target_os = "freebsd",
                  target_os = "haiku",
                  target_os = "ios",
                  target_os = "linux",
                  target_os = "redox",
                  target_os = "macos",
                  target_os = "netbsd"))]
        #[cfg_attr(docsrs, doc(cfg(all())))]
        WSTOPPED;
        /// Don't reap, just poll status.
        #[cfg(any(target_os = "android",
                  target_os = "freebsd",
                  target_os = "haiku",
                  target_os = "ios",
                  target_os = "linux",
                  target_os = "redox",
                  target_os = "macos",
                  target_os = "netbsd"))]
        #[cfg_attr(docsrs, doc(cfg(all())))]
        WNOWAIT;
        /// Don't wait on children of other threads in this group
        #[cfg(any(target_os = "android", target_os = "linux", target_os = "redox"))]
        #[cfg_attr(docsrs, doc(cfg(all())))]
        __WNOTHREAD;
        /// Wait on all children, regardless of type
        #[cfg(any(target_os = "android", target_os = "linux", target_os = "redox"))]
        #[cfg_attr(docsrs, doc(cfg(all())))]
        __WALL;
        /// Wait for "clone" children only.
        #[cfg(any(target_os = "android", target_os = "linux", target_os = "redox"))]
        #[cfg_attr(docsrs, doc(cfg(all())))]
        __WCLONE;
    }
);

/// Possible return values from `wait()` or `waitpid()`.
///
/// Each status (other than `StillAlive`) describes a state transition
/// in a child process `Pid`, such as the process exiting or stopping,
/// plus additional data about the transition if any.
///
/// Note that there are two Linux-specific enum variants, `PtraceEvent`
/// and `PtraceSyscall`. Portable code should avoid exhaustively
/// matching on `WaitStatus`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WaitStatus {
    /// The process exited normally (as with `exit()` or returning from
    /// `main`) with the given exit code. This case matches the C macro
    /// `WIFEXITED(status)`; the second field is `WEXITSTATUS(status)`.
    Exited(Pid, i32),
    /// The process was killed by the given signal. The third field
    /// indicates whether the signal generated a core dump. This case
    /// matches the C macro `WIFSIGNALED(status)`; the last two fields
    /// correspond to `WTERMSIG(status)` and `WCOREDUMP(status)`.
    Signaled(Pid, Signal, bool),
    /// The process is alive, but was stopped by the given signal. This
    /// is only reported if `WaitPidFlag::WUNTRACED` was passed. This
    /// case matches the C macro `WIFSTOPPED(status)`; the second field
    /// is `WSTOPSIG(status)`.
    Stopped(Pid, Signal),
    /// The traced process was stopped by a `PTRACE_EVENT_*` event. See
    /// [`nix::sys::ptrace`] and [`ptrace`(2)] for more information. All
    /// currently-defined events use `SIGTRAP` as the signal; the third
    /// field is the `PTRACE_EVENT_*` value of the event.
    ///
    /// [`nix::sys::ptrace`]: ../ptrace/index.html
    /// [`ptrace`(2)]: https://man7.org/linux/man-pages/man2/ptrace.2.html
    #[cfg(any(target_os = "linux", target_os = "android"))]
    #[cfg_attr(docsrs, doc(cfg(all())))]
    PtraceEvent(Pid, Signal, c_int),
    /// The traced process was stopped by execution of a system call,
    /// and `PTRACE_O_TRACESYSGOOD` is in effect. See [`ptrace`(2)] for
    /// more information.
    ///
    /// [`ptrace`(2)]: https://man7.org/linux/man-pages/man2/ptrace.2.html
    #[cfg(any(target_os = "linux", target_os = "android"))]
    #[cfg_attr(docsrs, doc(cfg(all())))]
    PtraceSyscall(Pid),
    /// The process was previously stopped but has resumed execution
    /// after receiving a `SIGCONT` signal. This is only reported if
    /// `WaitPidFlag::WCONTINUED` was passed. This case matches the C
    /// macro `WIFCONTINUED(status)`.
    Continued(Pid),
    /// There are currently no state changes to report in any awaited
    /// child process. This is only returned if `WaitPidFlag::WNOHANG`
    /// was used (otherwise `wait()` or `waitpid()` would block until
    /// there was something to report).
    StillAlive,
}

impl WaitStatus {
    /// Extracts the PID from the WaitStatus unless it equals StillAlive.
    pub fn pid(&self) -> Option<Pid> {
        use self::WaitStatus::*;
        match *self {
            Exited(p, _) | Signaled(p, _, _) | Stopped(p, _) | Continued(p) => {
                Some(p)
            }
            StillAlive => None,
            #[cfg(any(target_os = "android", target_os = "linux"))]
            PtraceEvent(p, _, _) | PtraceSyscall(p) => Some(p),
        }
    }
}

fn exited(status: i32) -> bool {
    libc::WIFEXITED(status)
}

fn exit_status(status: i32) -> i32 {
    libc::WEXITSTATUS(status)
}

fn signaled(status: i32) -> bool {
    libc::WIFSIGNALED(status)
}

fn term_signal(status: i32) -> Result<Signal> {
    Signal::try_from(libc::WTERMSIG(status))
}

fn dumped_core(status: i32) -> bool {
    libc::WCOREDUMP(status)
}

fn stopped(status: i32) -> bool {
    libc::WIFSTOPPED(status)
}

fn stop_signal(status: i32) -> Result<Signal> {
    Signal::try_from(libc::WSTOPSIG(status))
}

#[cfg(any(target_os = "android", target_os = "linux"))]
fn syscall_stop(status: i32) -> bool {
    // From ptrace(2), setting PTRACE_O_TRACESYSGOOD has the effect
    // of delivering SIGTRAP | 0x80 as the signal number for syscall
    // stops. This allows easily distinguishing syscall stops from
    // genuine SIGTRAP signals.
    libc::WSTOPSIG(status) == libc::SIGTRAP | 0x80
}

#[cfg(any(target_os = "android", target_os = "linux"))]
fn stop_additional(status: i32) -> c_int {
    (status >> 16) as c_int
}

fn continued(status: i32) -> bool {
    libc::WIFCONTINUED(status)
}

impl WaitStatus {
    /// Convert a raw `wstatus` as returned by `waitpid`/`wait` into a `WaitStatus`
    ///
    /// # Errors
    ///
    /// Returns an `Error` corresponding to `EINVAL` for invalid status values.
    ///
    /// # Examples
    ///
    /// Convert a `wstatus` obtained from `libc::waitpid` into a `WaitStatus`:
    ///
    /// ```
    /// use nix::sys::wait::WaitStatus;
    /// use nix::sys::signal::Signal;
    /// let pid = nix::unistd::Pid::from_raw(1);
    /// let status = WaitStatus::from_raw(pid, 0x0002);
    /// assert_eq!(status, Ok(WaitStatus::Signaled(pid, Signal::SIGINT, false)));
    /// ```
    pub fn from_raw(pid: Pid, status: i32) -> Result<WaitStatus> {
        Ok(if exited(status) {
            WaitStatus::Exited(pid, exit_status(status))
        } else if signaled(status) {
            WaitStatus::Signaled(pid, term_signal(status)?, dumped_core(status))
        } else if stopped(status) {
            cfg_if! {
                if #[cfg(any(target_os = "android", target_os = "linux"))] {
                    fn decode_stopped(pid: Pid, status: i32) -> Result<WaitStatus> {
                        let status_additional = stop_additional(status);
                        Ok(if syscall_stop(status) {
                            WaitStatus::PtraceSyscall(pid)
                        } else if status_additional == 0 {
                            WaitStatus::Stopped(pid, stop_signal(status)?)
                        } else {
                            WaitStatus::PtraceEvent(pid, stop_signal(status)?,
                                                    stop_additional(status))
                        })
                    }
                } else {
                    fn decode_stopped(pid: Pid, status: i32) -> Result<WaitStatus> {
                        Ok(WaitStatus::Stopped(pid, stop_signal(status)?))
                    }
                }
            }
            return decode_stopped(pid, status);
        } else {
            assert!(continued(status));
            WaitStatus::Continued(pid)
        })
    }

    /// Convert a `siginfo_t` as returned by `waitid` to a `WaitStatus`
    ///
    /// # Errors
    ///
    /// Returns an `Error` corresponding to `EINVAL` for invalid values.
    ///
    /// # Safety
    ///
    /// siginfo_t is actually a union, not all fields may be initialized.
    /// The functions si_pid() and si_status() must be valid to call on
    /// the passed siginfo_t.
    #[cfg(any(
        target_os = "android",
        target_os = "freebsd",
        target_os = "haiku",
        all(target_os = "linux", not(target_env = "uclibc")),
    ))]
    unsafe fn from_siginfo(siginfo: &libc::siginfo_t) -> Result<WaitStatus> {
        let si_pid = siginfo.si_pid();
        if si_pid == 0 {
            return Ok(WaitStatus::StillAlive);
        }

        assert_eq!(siginfo.si_signo, libc::SIGCHLD);

        let pid = Pid::from_raw(si_pid);
        let si_status = siginfo.si_status();

        let status = match siginfo.si_code {
            libc::CLD_EXITED => WaitStatus::Exited(pid, si_status),
            libc::CLD_KILLED | libc::CLD_DUMPED => WaitStatus::Signaled(
                pid,
                Signal::try_from(si_status)?,
                siginfo.si_code == libc::CLD_DUMPED,
            ),
            libc::CLD_STOPPED => {
                WaitStatus::Stopped(pid, Signal::try_from(si_status)?)
            }
            libc::CLD_CONTINUED => WaitStatus::Continued(pid),
            #[cfg(any(target_os = "android", target_os = "linux"))]
            libc::CLD_TRAPPED => {
                if si_status == libc::SIGTRAP | 0x80 {
                    WaitStatus::PtraceSyscall(pid)
                } else {
                    WaitStatus::PtraceEvent(
                        pid,
                        Signal::try_from(si_status & 0xff)?,
                        (si_status >> 8) as c_int,
                    )
                }
            }
            _ => return Err(Errno::EINVAL),
        };

        Ok(status)
    }
}

/// Wait for a process to change status
///
/// See also [waitpid(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/waitpid.html)
pub fn waitpid<P: Into<Option<Pid>>>(
    pid: P,
    options: Option<WaitPidFlag>,
) -> Result<WaitStatus> {
    use self::WaitStatus::*;

    let mut status: i32 = 0;

    let option_bits = match options {
        Some(bits) => bits.bits(),
        None => 0,
    };

    let res = unsafe {
        libc::waitpid(
            pid.into().unwrap_or_else(|| Pid::from_raw(-1)).into(),
            &mut status as *mut c_int,
            option_bits,
        )
    };

    match Errno::result(res)? {
        0 => Ok(StillAlive),
        res => WaitStatus::from_raw(Pid::from_raw(res), status),
    }
}

/// Wait for any child process to change status or a signal is received.
///
/// See also [wait(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/wait.html)
pub fn wait() -> Result<WaitStatus> {
    waitpid(None, None)
}

/// The ID argument for `waitid`
#[cfg(any(
    target_os = "android",
    target_os = "freebsd",
    target_os = "haiku",
    all(target_os = "linux", not(target_env = "uclibc")),
))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Id {
    /// Wait for any child
    All,
    /// Wait for the child whose process ID matches the given PID
    Pid(Pid),
    /// Wait for the child whose process group ID matches the given PID
    ///
    /// If the PID is zero, the caller's process group is used since Linux 5.4.
    PGid(Pid),
    /// Wait for the child referred to by the given PID file descriptor
    #[cfg(any(target_os = "android", target_os = "linux"))]
    PIDFd(RawFd),
}

/// Wait for a process to change status
///
/// See also [waitid(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/waitid.html)
#[cfg(any(
    target_os = "android",
    target_os = "freebsd",
    target_os = "haiku",
    all(target_os = "linux", not(target_env = "uclibc")),
))]
pub fn waitid(id: Id, flags: WaitPidFlag) -> Result<WaitStatus> {
    let (idtype, idval) = match id {
        Id::All => (libc::P_ALL, 0),
        Id::Pid(pid) => (libc::P_PID, pid.as_raw() as libc::id_t),
        Id::PGid(pid) => (libc::P_PGID, pid.as_raw() as libc::id_t),
        #[cfg(any(target_os = "android", target_os = "linux"))]
        Id::PIDFd(fd) => (libc::P_PIDFD, fd as libc::id_t),
    };

    let siginfo = unsafe {
        // Memory is zeroed rather than uninitialized, as not all platforms
        // initialize the memory in the StillAlive case
        let mut siginfo: libc::siginfo_t = std::mem::zeroed();
        Errno::result(libc::waitid(idtype, idval, &mut siginfo, flags.bits()))?;
        siginfo
    };

    unsafe { WaitStatus::from_siginfo(&siginfo) }
}
