anitrack 0.1.7

CLI/TUI companion for ani-cli with watch-progress tracking
use std::process::{Command as ProcessCommand, ExitStatus};

#[cfg(unix)]
use anyhow::anyhow;
use anyhow::{Context, Result};

#[cfg(unix)]
use std::os::unix::process::CommandExt;

#[cfg(unix)]
struct ScopedSigaction {
    signum: libc::c_int,
    old_action: libc::sigaction,
}

#[cfg(unix)]
impl ScopedSigaction {
    fn ignore(signum: libc::c_int) -> Result<Self> {
        unsafe {
            let mut new_action: libc::sigaction = std::mem::zeroed();
            new_action.sa_sigaction = libc::SIG_IGN;
            libc::sigemptyset(&mut new_action.sa_mask);
            new_action.sa_flags = 0;

            let mut old_action: libc::sigaction = std::mem::zeroed();
            if libc::sigaction(signum, &new_action, &mut old_action) != 0 {
                return Err(anyhow!("failed to update signal action for {signum}"));
            }

            Ok(Self { signum, old_action })
        }
    }
}

#[cfg(unix)]
impl Drop for ScopedSigaction {
    fn drop(&mut self) {
        unsafe {
            let _ = libc::sigaction(self.signum, &self.old_action, std::ptr::null_mut());
        }
    }
}

#[cfg(unix)]
struct TerminalForegroundGuard {
    stdin_fd: libc::c_int,
    parent_pgrp: libc::pid_t,
    child_foreground: bool,
}

#[cfg(unix)]
impl TerminalForegroundGuard {
    fn new(stdin_fd: libc::c_int, parent_pgrp: libc::pid_t) -> Self {
        Self {
            stdin_fd,
            parent_pgrp,
            child_foreground: false,
        }
    }

    fn handoff_to_child(&mut self, child_pgrp: libc::pid_t) {
        self.child_foreground = unsafe { libc::tcsetpgrp(self.stdin_fd, child_pgrp) == 0 };
    }
}

#[cfg(unix)]
impl Drop for TerminalForegroundGuard {
    fn drop(&mut self) {
        if !self.child_foreground {
            return;
        }
        unsafe {
            let _ = libc::tcsetpgrp(self.stdin_fd, self.parent_pgrp);
        }
    }
}

#[cfg(unix)]
pub(crate) fn with_sigint_ignored<F, R>(f: F) -> Result<R>
where
    F: FnOnce() -> Result<R>,
{
    let _sigint_guard = ScopedSigaction::ignore(libc::SIGINT)?;
    f()
}

#[cfg(not(unix))]
pub(crate) fn with_sigint_ignored<F, R>(f: F) -> Result<R>
where
    F: FnOnce() -> Result<R>,
{
    f()
}

#[cfg(unix)]
pub(crate) fn run_interactive_cmd(mut cmd: ProcessCommand) -> Result<ExitStatus> {
    let stdin_fd = libc::STDIN_FILENO;
    let parent_pgrp = unsafe { libc::tcgetpgrp(stdin_fd) };
    if parent_pgrp == -1 {
        return cmd.status().context("failed to launch ani-cli");
    }

    let _sigttou_guard = ScopedSigaction::ignore(libc::SIGTTOU)?;
    let mut terminal_guard = TerminalForegroundGuard::new(stdin_fd, parent_pgrp);

    unsafe {
        cmd.pre_exec(|| {
            libc::signal(libc::SIGINT, libc::SIG_DFL);
            libc::signal(libc::SIGQUIT, libc::SIG_DFL);
            libc::signal(libc::SIGTSTP, libc::SIG_DFL);
            if libc::setpgid(0, 0) != 0 {
                return Err(std::io::Error::last_os_error());
            }
            Ok(())
        });
    }

    let mut child = cmd.spawn().context("failed to spawn ani-cli")?;
    let child_pgid = child.id() as libc::pid_t;
    terminal_guard.handoff_to_child(child_pgid);
    child.wait().context("failed waiting on ani-cli")
}

#[cfg(not(unix))]
pub(crate) fn run_interactive_cmd(mut cmd: ProcessCommand) -> Result<ExitStatus> {
    cmd.status().context("failed to launch ani-cli")
}