/*! Tools to execute (process) a job */

/*
    Copyright (C) 2022 John Goerzen <jgoerzen@complete.org>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

use anyhow;
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::process::ExitStatusExt;
use std::process::{Command, ExitStatus, Stdio};
use std::time::Duration;
use tracing::*;
use wait_timeout::ChildExt;

/** Low-level code to execute a job.  See [`crate::cmd::cmd_exec`] for
higher-level interfaces. */
pub fn exec_job<T: Into<Stdio>>(
    command: &OsString,
    params: &[OsString],
    env: Vec<(Vec<u8>, Vec<u8>)>,
    payload: Option<T>,
    stdoutput: Stdio,
    stderr: Stdio,
    timeoutsecs: Option<u64>,
) -> Result<ExitStatus, anyhow::Error> {
    debug!("Preparing to run {:?} with params {:?}", command, params);
    let mut command = Command::new(command);
    command
        .args(params)
        .envs(
            env.iter()
                .map(|(k, v)| (OsStr::from_bytes(k), OsStr::from_bytes(v))),
        )
        .stdout(stdoutput)
        .stderr(stderr);
    if let Some(payload) = payload {
        command.stdin(payload);
    }
    let mut child = command.spawn()?;
    debug!("Command PID {} started successfully", child.id());

    // Explicitly drop to release held FDs.
    std::mem::drop(command);

    let exitstatus = if let Some(timeout) = timeoutsecs {
        debug!("Waiting up to {} seconds for command to exit", timeout);
        match child.wait_timeout(Duration::from_secs(timeout))? {
            None => {
                // Timeout elapsed without the child exiting
                debug!("Command timed out; sending termination signal");
                child.kill()?;
                // Now give it a chance to die
                std::thread::sleep(Duration::from_millis(500));
                match child.try_wait()? {
                    Some(r) => {
                        debug!("Command terminated after termination signal");
                        r
                    }
                    None => {
                        debug!("Command didn't terminate even after signal; proceeding with an error report anyway");
                        ExitStatus::from_raw(0x7f) // Fake an error exit status since the child didn't die
                    }
                }
            }
            Some(r) => r, // Child did exit
        }
    } else {
        debug!("Waiting indefinitely for command to exit");
        child.wait()?
    };

    if exitstatus.success() {
        debug!("Command exited successfully with status {:?}", exitstatus);
    } else {
        error!("Command exited abnormally with status {:?}", exitstatus);
    }

    Ok(exitstatus)
}
