regy 0.1.0

Private-by-default desktop agent for the Regy web interface
use std::io;
use std::os::fd::RawFd;
use std::process::Child;

use nix::libc;
use tokio::time::{Duration, sleep};

use crate::domain::errors::{AgentError, AgentResult, ErrorCode};
use crate::infrastructure::pty_session::signal_failure_should_be_ignored;

pub(crate) fn configure_child_terminal(slave_fd: RawFd) -> io::Result<()> {
    match unsafe { libc::setsid() } {
        -1 => {
            let err = io::Error::last_os_error();
            if err.raw_os_error() == Some(libc::EPERM) {
                if unsafe { libc::setpgid(0, 0) } == -1 {
                    return Err(io::Error::last_os_error());
                }
                acquire_controlling_terminal(slave_fd)
            } else {
                Err(err)
            }
        }
        _ => acquire_controlling_terminal(slave_fd),
    }
}

pub(crate) async fn terminate_child(child: &mut Child, pgid: i32) -> AgentResult<()> {
    let _ = child.try_wait().map_err(|e| kill_err(e.to_string()))?;
    if let Err(err) = send_signal(-pgid, libc::SIGTERM) {
        handle_signal_error(child, err)?;
    }
    sleep(Duration::from_millis(100)).await;
    if let Err(err) = send_signal(-pgid, libc::SIGKILL) {
        handle_signal_error(child, err)?;
    }
    wait_for_exit(child).await
}

fn acquire_controlling_terminal(slave_fd: RawFd) -> io::Result<()> {
    #[cfg(target_os = "linux")]
    let request: libc::c_ulong = libc::TIOCSCTTY;
    #[cfg(not(target_os = "linux"))]
    let request: libc::c_ulong = libc::TIOCSCTTY.into();

    if unsafe { libc::ioctl(slave_fd, request, 0) } == -1 {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

fn handle_signal_error(child: &mut Child, err: io::Error) -> AgentResult<()> {
    let leader_exited = child
        .try_wait()
        .map_err(|e| kill_err(e.to_string()))?
        .is_some();
    if signal_failure_should_be_ignored(err.raw_os_error(), leader_exited) {
        Ok(())
    } else {
        Err(kill_err(err.to_string()))
    }
}

async fn wait_for_exit(child: &mut Child) -> AgentResult<()> {
    loop {
        match child.try_wait().map_err(|e| kill_err(e.to_string()))? {
            Some(_) => return Ok(()),
            None => sleep(Duration::from_millis(20)).await,
        }
    }
}

fn send_signal(pid: i32, signal: i32) -> io::Result<()> {
    match unsafe { libc::kill(pid, signal) } {
        0 => Ok(()),
        _ => Err(io::Error::last_os_error()),
    }
}

fn kill_err(message: impl Into<String>) -> AgentError {
    AgentError::new(ErrorCode::PtyKillFailed, message)
}