use std::ffi::{OsStr, OsString};
use std::fmt;
use std::io;
use std::path::Path;
use std::process::{Child, Command};
use tokio::task::LocalSet;
use super::session::Session;
use super::watch::Watch;
use super::{Attended, Kept, Layout, Rig};
use crate::failure::{Doing, Failure};
pub struct Run<K> {
pub shells: Vec<Attended<K>>,
pub subject: ExitStatus,
pub failed: Option<Failure>,
}
impl<K> Run<K> {
pub fn whole(self) -> Result<Whole<K>, Failure> {
match self.failed {
Some(why) => Err(why),
None => Ok(Whole {
shells: self.shells,
subject: self.subject,
}),
}
}
}
pub struct Whole<K> {
pub shells: Vec<Attended<K>>,
pub subject: ExitStatus,
}
#[expect(async_fn_in_trait, reason = "single-threaded by design: no Send bound")]
pub trait Driving: Rig {
async fn run<A, E>(&self, argv: &[A], environment: E) -> Result<Run<Kept<Self>>, Failure>
where
A: AsRef<OsStr>,
E: FnOnce(&Layout) -> Result<Vec<(OsString, OsString)>, Failure>,
Self: Sized,
{
driven(self, None, argv, environment).await
}
async fn run_at<A, E>(&self, at: &Path, argv: &[A], environment: E) -> Result<Run<Kept<Self>>, Failure>
where
A: AsRef<OsStr>,
E: FnOnce(&Layout) -> Result<Vec<(OsString, OsString)>, Failure>,
Self: Sized,
{
driven(self, Some(at), argv, environment).await
}
}
async fn driven<R, A, E>(rig: &R, at: Option<&Path>, argv: &[A], environment: E) -> Result<Run<Kept<R>>, Failure>
where
R: Rig,
A: AsRef<OsStr>,
E: FnOnce(&Layout) -> Result<Vec<(OsString, OsString)>, Failure>,
{
LocalSet::new()
.run_until(async {
let mut session = Session::open(rig, at)?;
let subject = async {
let environment = environment(&session.layout)?;
let mut subject = Subject::spawn(argv, environment)?;
session.serve(&Watch::process(subject.pid())?).await?;
subject.finish().doing(|| "waiting for bash".into())
}
.await;
let (shells, failed) = session.close().await;
let subject = subject?;
Ok(Run {
shells,
subject: ExitStatus::from(subject),
failed,
})
})
.await
}
struct Subject {
child: Child,
group: libc::pid_t,
}
impl Subject {
fn spawn<A: AsRef<OsStr>>(argv: &[A], environment: Vec<(OsString, OsString)>) -> Result<Self, Failure> {
use std::os::unix::process::CommandExt;
let said = || {
argv.iter()
.map(|word| word.as_ref().to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
};
let (program, rest) = argv.split_first().ok_or_else(|| {
Failure::new(
"starting the subject",
"the command line is empty",
)
})?;
let mut command = Command::new(program);
command.args(rest).envs(environment).process_group(0);
let child = command.spawn().doing(|| format!("spawning {}", said()))?;
let group = child.id() as libc::pid_t;
Ok(Self { child, group })
}
fn pid(&self) -> libc::pid_t {
self.group
}
fn finish(&mut self) -> io::Result<std::process::ExitStatus> {
self.release();
self.child.wait()
}
fn release(&self) {
let _ = unsafe { libc::kill(-self.group, libc::SIGKILL) };
}
}
impl Drop for Subject {
fn drop(&mut self) {
self.release();
let _ = self.child.wait();
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ExitStatus {
Code(u8),
Signal(u8),
}
impl ExitStatus {
pub fn shell_code(self) -> i32 {
match self {
Self::Code(code) => i32::from(code),
Self::Signal(signal) => 128 + i32::from(signal),
}
}
}
impl fmt::Display for ExitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Code(code) => write!(f, "exit {code}"),
Self::Signal(signal) => write!(f, "killed by signal {signal}"),
}
}
}
impl From<std::process::ExitStatus> for ExitStatus {
fn from(status: std::process::ExitStatus) -> Self {
use std::os::unix::process::ExitStatusExt;
let raw = status.into_raw();
match status.signal() {
Some(_) => Self::Signal((raw & 0x7f) as u8),
None => Self::Code(((raw >> 8) & 0xff) as u8),
}
}
}