bash_interop/rig/
driving.rs1use std::ffi::{OsStr, OsString};
5use std::fmt;
6use std::io;
7use std::path::Path;
8use std::process::{Child, Command};
9
10use tokio::task::LocalSet;
11
12use super::session::Session;
13use super::watch::Watch;
14use super::{Attended, Kept, Layout, Rig};
15use crate::failure::{Doing, Failure};
16
17pub struct Run<K> {
23 pub shells: Vec<Attended<K>>,
25
26 pub subject: ExitStatus,
28
29 pub failed: Option<Failure>,
32}
33
34impl<K> Run<K> {
35 pub fn whole(self) -> Result<Whole<K>, Failure> {
37 match self.failed {
38 Some(why) => Err(why),
39 None => Ok(Whole {
40 shells: self.shells,
41 subject: self.subject,
42 }),
43 }
44 }
45}
46
47pub struct Whole<K> {
49 pub shells: Vec<Attended<K>>,
50 pub subject: ExitStatus,
51}
52
53#[expect(async_fn_in_trait, reason = "single-threaded by design: no Send bound")]
74pub trait Driving: Rig {
75 async fn run<A, E>(&self, argv: &[A], environment: E) -> Result<Run<Kept<Self>>, Failure>
77 where
78 A: AsRef<OsStr>,
79 E: FnOnce(&Layout) -> Result<Vec<(OsString, OsString)>, Failure>,
80 Self: Sized,
81 {
82 driven(self, None, argv, environment).await
83 }
84
85 async fn run_at<A, E>(&self, at: &Path, argv: &[A], environment: E) -> Result<Run<Kept<Self>>, Failure>
89 where
90 A: AsRef<OsStr>,
91 E: FnOnce(&Layout) -> Result<Vec<(OsString, OsString)>, Failure>,
92 Self: Sized,
93 {
94 driven(self, Some(at), argv, environment).await
95 }
96}
97
98async fn driven<R, A, E>(rig: &R, at: Option<&Path>, argv: &[A], environment: E) -> Result<Run<Kept<R>>, Failure>
100where
101 R: Rig,
102 A: AsRef<OsStr>,
103 E: FnOnce(&Layout) -> Result<Vec<(OsString, OsString)>, Failure>,
104{
105 LocalSet::new()
106 .run_until(async {
107 let mut session = Session::open(rig, at)?;
108
109 let subject = async {
112 let environment = environment(&session.layout)?;
113 let mut subject = Subject::spawn(argv, environment)?;
114
115 session.serve(&Watch::process(subject.pid())?).await?;
116 subject.finish().doing(|| "waiting for bash".into())
117 }
118 .await;
119 let (shells, failed) = session.close().await;
120 let subject = subject?;
121
122 Ok(Run {
123 shells,
124 subject: ExitStatus::from(subject),
125 failed,
126 })
127 })
128 .await
129}
130
131struct Subject {
133 child: Child,
134 group: libc::pid_t,
135}
136
137impl Subject {
138 fn spawn<A: AsRef<OsStr>>(argv: &[A], environment: Vec<(OsString, OsString)>) -> Result<Self, Failure> {
139 use std::os::unix::process::CommandExt;
140
141 let said = || {
142 argv.iter()
143 .map(|word| word.as_ref().to_string_lossy())
144 .collect::<Vec<_>>()
145 .join(" ")
146 };
147 let (program, rest) = argv.split_first().ok_or_else(|| {
148 Failure::new(
149 "starting the subject",
150 "the command line is empty",
151 )
152 })?;
153
154 let mut command = Command::new(program);
155 command.args(rest).envs(environment).process_group(0);
156
157 let child = command.spawn().doing(|| format!("spawning {}", said()))?;
158 let group = child.id() as libc::pid_t;
159
160 Ok(Self { child, group })
161 }
162
163 fn pid(&self) -> libc::pid_t {
164 self.group
165 }
166
167 fn finish(&mut self) -> io::Result<std::process::ExitStatus> {
170 self.release();
171 self.child.wait()
172 }
173
174 fn release(&self) {
175 let _ = unsafe { libc::kill(-self.group, libc::SIGKILL) };
176 }
177}
178
179impl Drop for Subject {
180 fn drop(&mut self) {
181 self.release();
182 let _ = self.child.wait();
185 }
186}
187
188#[derive(Copy, Clone, PartialEq, Eq, Debug)]
190pub enum ExitStatus {
191 Code(u8),
192 Signal(u8),
193}
194
195impl ExitStatus {
196 pub fn shell_code(self) -> i32 {
198 match self {
199 Self::Code(code) => i32::from(code),
200 Self::Signal(signal) => 128 + i32::from(signal),
201 }
202 }
203}
204
205impl fmt::Display for ExitStatus {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 match self {
208 Self::Code(code) => write!(f, "exit {code}"),
209 Self::Signal(signal) => write!(f, "killed by signal {signal}"),
210 }
211 }
212}
213
214impl From<std::process::ExitStatus> for ExitStatus {
215 fn from(status: std::process::ExitStatus) -> Self {
218 use std::os::unix::process::ExitStatusExt;
219
220 let raw = status.into_raw();
221 match status.signal() {
222 Some(_) => Self::Signal((raw & 0x7f) as u8),
223 None => Self::Code(((raw >> 8) & 0xff) as u8),
224 }
225 }
226}