1use std::io::{self, Write};
12use std::sync::mpsc;
13use std::time::Duration;
14
15use signal_hook::consts::signal::{SIGHUP, SIGINT, SIGTERM};
16
17use crate::error::Result;
18use crate::exec::{ExecEvent, ExecOptions};
19use crate::sandbox::Sandbox;
20
21pub struct Service {
23 pub name: String,
25 pub sandbox: Sandbox,
26 pub run: Run,
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum Run {
32 Hold,
34 Command(Vec<String>),
37 Follow(Vec<String>),
40 Console,
42}
43
44#[derive(Debug, Clone, Copy)]
45pub struct Options {
46 pub log_prefix: bool,
48 pub stop_timeout: Duration,
50 pub poll: Duration,
52}
53
54impl Default for Options {
55 fn default() -> Self {
56 Options {
57 log_prefix: true,
58 stop_timeout: Duration::from_secs(10),
59 poll: Duration::from_secs(1),
60 }
61 }
62}
63
64enum Msg {
65 Signal(i32),
66 Orphaned(u32),
67 OutputClosed,
68 Exited(usize, Result<i32>),
69 Stopped,
70}
71
72#[expect(
77 clippy::too_many_lines,
78 reason = "predates the lint ratchet; split it when next changed"
79)]
80pub fn run(services: &[Service], opts: Options, report: &mut dyn FnMut(&str)) -> Result<u8> {
81 let (tx, rx) = mpsc::channel::<Msg>();
82
83 let mut signals = signal_hook::iterator::Signals::new([SIGINT, SIGTERM, SIGHUP])?;
84 let sig_handle = signals.handle();
85 {
86 let tx = tx.clone();
87 std::thread::spawn(move || {
88 for sig in signals.forever() {
89 if tx.send(Msg::Signal(sig)).is_err() {
90 break;
91 }
92 }
93 });
94 }
95
96 let ancestors = ancestors();
97 {
98 let tx = tx.clone();
99 std::thread::spawn(move || {
100 loop {
101 std::thread::sleep(opts.poll);
102 if let Some(&(pid, _)) = ancestors.iter().find(|&&(p, t)| !alive(p, t)) {
103 let _ = tx.send(Msg::Orphaned(pid));
104 break;
105 }
106 }
107 });
108 }
109
110 let mut running = 0usize;
111 let mut followed = 0usize;
112 for (i, svc) in services.iter().enumerate() {
113 let prefix = opts.log_prefix.then(|| format!("{} | ", svc.name));
114 let (argv, eopts) = match &svc.run {
115 Run::Hold => continue,
116 Run::Console => {
117 followed += 1;
118 follow_console(svc.sandbox.clone(), prefix, tx.clone());
119 continue;
120 }
121 Run::Follow(argv) => (argv, ExecOptions::default().user("root").cwd("/")),
122 Run::Command(argv) => (argv, ExecOptions::default()),
123 };
124 let counts = matches!(svc.run, Run::Command(_));
125 let stream = match svc.sandbox.exec_stream(argv.clone(), eopts) {
126 Ok(s) => s,
127 Err(e) if counts => {
128 report(&format!("{}: command failed to start: {e}", svc.name));
129 let _ = tx.send(Msg::Exited(i, Err(e)));
130 running += 1;
131 continue;
132 }
133 Err(e) => {
134 report(&format!("{}: cannot follow its log: {e}", svc.name));
135 continue;
136 }
137 };
138 if counts {
139 running += 1;
140 } else {
141 followed += 1;
142 }
143 let tx = tx.clone();
144 std::thread::spawn(move || {
145 let mut out = LineWriter::new(prefix.clone());
146 let mut err = LineWriter::new(prefix);
147 let mut stream = stream;
148 let mut closed = false;
149 while let Some(ev) = stream.next_event() {
150 let r = match ev {
151 ExecEvent::Stdout(b) => out.write(&mut io::stdout().lock(), &b),
152 ExecEvent::Stderr(b) => err.write(&mut io::stderr().lock(), &b),
153 };
154 if r.is_err() && !closed {
155 closed = true;
156 let _ = tx.send(Msg::OutputClosed);
157 }
158 }
159 let _ = out.finish(&mut io::stdout().lock());
160 let _ = err.finish(&mut io::stderr().lock());
161 let r = stream.wait();
162 if counts {
163 let _ = tx.send(Msg::Exited(i, r));
164 }
165 });
166 }
167
168 if running == 0 {
169 let names: Vec<&str> = services.iter().map(|s| s.name.as_str()).collect();
170 let why = if followed > 0 {
171 "supervised, so only Ctrl-C ends this"
172 } else {
173 "no command, so nothing else will"
174 };
175 report(&format!("{}: up; Ctrl-C stops ({why})", names.join(", ")));
176 }
177
178 let mut first_failure: Option<i32> = None;
179 let code: u8 = loop {
180 match rx.recv() {
181 Ok(Msg::Exited(i, r)) => {
182 let name = &services[i].name;
183 match r {
184 Ok(c) => {
185 report(&format!("{name}: command exited with code {c}"));
186 if c != 0 && first_failure.is_none() {
187 first_failure = Some(c);
188 }
189 }
190 Err(e) => {
191 report(&format!("{name}: command failed: {e}"));
192 first_failure.get_or_insert(1);
193 }
194 }
195 running -= 1;
196 if running == 0 {
197 break first_failure.map_or(0, |c| c.clamp(1, 255) as u8);
198 }
199 }
200 Ok(Msg::Signal(s)) => break 128 + s as u8,
201 Ok(Msg::Orphaned(pid)) => {
202 report(&format!("process {pid}, which started isb, has exited"));
203 break 129;
204 }
205 Ok(Msg::OutputClosed) => break 141,
206 Ok(Msg::Stopped) => unreachable!("stop has not started"),
207 Err(_) => break 1,
208 }
209 };
210
211 let handles: Vec<Sandbox> = services.iter().map(|s| s.sandbox.clone()).collect();
213 let names: Vec<String> = services.iter().map(|s| s.name.clone()).collect();
214 for n in &names {
215 report(&format!("{n}: stopping"));
216 }
217 {
218 let tx = tx.clone();
219 let handles = handles.clone();
220 std::thread::spawn(move || {
221 for sb in &handles {
222 if sb.stop(false, opts.stop_timeout).is_err() {
223 let _ = sb.stop(true, opts.stop_timeout);
224 }
225 }
226 let _ = tx.send(Msg::Stopped);
227 });
228 }
229 loop {
230 match rx.recv() {
231 Ok(Msg::Stopped) | Err(_) => break,
232 Ok(Msg::Signal(_)) => {
233 report("forcing stop");
234 for sb in &handles {
235 let _ = sb.stop(true, opts.stop_timeout);
236 }
237 break;
238 }
239 Ok(_) => {}
240 }
241 }
242 for n in &names {
243 report(&format!("{n}: stopped"));
244 }
245 sig_handle.close();
246 Ok(code)
247}
248
249fn follow_console(sb: Sandbox, prefix: Option<String>, tx: mpsc::Sender<Msg>) {
253 std::thread::spawn(move || {
254 let mut out = LineWriter::new(prefix);
255 let mut seen = sb
257 .client()
258 .console_log(sb.name())
259 .map(|b| b.len())
260 .unwrap_or(0);
261 loop {
262 std::thread::sleep(Duration::from_secs(1));
263 let Ok(log) = sb.client().console_log(sb.name()) else {
264 continue;
265 };
266 if log.len() < seen {
267 seen = 0;
268 }
269 if log.len() > seen {
270 if out.write(&mut io::stdout().lock(), &log[seen..]).is_err() {
271 let _ = tx.send(Msg::OutputClosed);
272 return;
273 }
274 seen = log.len();
275 }
276 }
277 });
278}
279
280struct LineWriter {
282 prefix: Option<String>,
283 at_line_start: bool,
284}
285
286impl LineWriter {
287 fn new(prefix: Option<String>) -> Self {
288 LineWriter {
289 prefix,
290 at_line_start: true,
291 }
292 }
293
294 fn write(&mut self, w: &mut impl Write, buf: &[u8]) -> io::Result<()> {
295 let Some(prefix) = &self.prefix else {
296 w.write_all(buf)?;
297 return w.flush();
298 };
299 for line in buf.split_inclusive(|&b| b == b'\n') {
300 if self.at_line_start {
301 w.write_all(prefix.as_bytes())?;
302 }
303 w.write_all(line)?;
304 self.at_line_start = line.ends_with(b"\n");
305 }
306 w.flush()
307 }
308
309 fn finish(&mut self, w: &mut impl Write) -> io::Result<()> {
311 if self.prefix.is_some() && !self.at_line_start {
312 self.at_line_start = true;
313 w.write_all(b"\n")?;
314 return w.flush();
315 }
316 Ok(())
317 }
318}
319
320fn ancestors() -> Vec<(u32, u64)> {
323 let mut out = Vec::new();
324 let Some((mut ppid, _)) = stat(std::process::id()) else {
325 return out;
326 };
327 while ppid > 1 && out.len() < 64 {
328 let Some((next, start)) = stat(ppid) else {
329 break;
330 };
331 out.push((ppid, start));
332 ppid = next;
333 }
334 out
335}
336
337fn alive(pid: u32, start: u64) -> bool {
338 matches!(stat(pid), Some((_, s)) if s == start)
339}
340
341#[cfg(target_os = "linux")]
344fn stat(pid: u32) -> Option<(u32, u64)> {
345 let s = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
346 parse_stat(&s)
347}
348
349#[cfg(target_os = "macos")]
352fn stat(pid: u32) -> Option<(u32, u64)> {
353 let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
354 let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
355 let n = unsafe {
357 libc::proc_pidinfo(
358 libc::pid_t::try_from(pid).ok()?,
359 libc::PROC_PIDTBSDINFO,
360 0,
361 info.as_mut_ptr().cast(),
362 size,
363 )
364 };
365 if n != size {
366 return None;
367 }
368 let info = unsafe { info.assume_init() };
370 if info.pbi_status == libc::SZOMB {
371 return None;
372 }
373 Some((
374 info.pbi_ppid,
375 info.pbi_start_tvsec * 1_000_000 + info.pbi_start_tvusec,
376 ))
377}
378
379#[cfg(target_os = "linux")]
380fn parse_stat(s: &str) -> Option<(u32, u64)> {
381 let rest = &s[s.rfind(')')? + 1..];
384 let f: Vec<&str> = rest.split_whitespace().collect();
385 if matches!(f.first(), Some(&"Z") | Some(&"X")) {
386 return None;
387 }
388 Some((f.get(1)?.parse().ok()?, f.get(19)?.parse().ok()?))
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[cfg(target_os = "linux")]
396 #[test]
397 fn parses_stat() {
398 let line =
399 "1234 (my (odd) cmd) S 99 1234 1234 0 -1 4194304 1 0 0 0 0 0 0 0 20 0 1 0 5555 0 0";
400 assert_eq!(parse_stat(line), Some((99, 5555)));
401 let zombie = "1234 (x) Z 99 1234 1234 0 -1 4194304 1 0 0 0 0 0 0 0 20 0 1 0 5555 0 0";
402 assert_eq!(parse_stat(zombie), None);
403 }
404
405 #[test]
406 fn own_ancestors_are_alive() {
407 let a = ancestors();
408 assert!(!a.is_empty());
409 assert!(a.iter().all(|&(p, t)| alive(p, t)));
410 assert!(!alive(a[0].0, a[0].1 + 1));
411 }
412
413 #[test]
414 fn prefixes_every_line() {
415 let mut w = LineWriter::new(Some("web | ".into()));
416 let mut out = Vec::new();
417 w.write(&mut out, b"one\ntw").unwrap();
418 w.write(&mut out, b"o\nthree").unwrap();
419 w.finish(&mut out).unwrap();
420 assert_eq!(out, b"web | one\nweb | two\nweb | three\n");
421 }
422}