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>) {
252 std::thread::spawn(move || {
253 let mut out = LineWriter::new(prefix);
254 let mut pos = sb
256 .client()
257 .console_since(sb.name(), u64::MAX)
258 .map_or(0, |(_, end)| end);
259 loop {
260 std::thread::sleep(Duration::from_secs(1));
261 let Ok((new, end)) = sb.client().console_since(sb.name(), pos) else {
262 continue;
263 };
264 pos = end;
265 if !new.is_empty() && out.write(&mut io::stdout().lock(), &new).is_err() {
266 let _ = tx.send(Msg::OutputClosed);
267 return;
268 }
269 }
270 });
271}
272
273struct LineWriter {
275 prefix: Option<String>,
276 at_line_start: bool,
277}
278
279impl LineWriter {
280 fn new(prefix: Option<String>) -> Self {
281 LineWriter {
282 prefix,
283 at_line_start: true,
284 }
285 }
286
287 fn write(&mut self, w: &mut impl Write, buf: &[u8]) -> io::Result<()> {
288 let Some(prefix) = &self.prefix else {
289 w.write_all(buf)?;
290 return w.flush();
291 };
292 for line in buf.split_inclusive(|&b| b == b'\n') {
293 if self.at_line_start {
294 w.write_all(prefix.as_bytes())?;
295 }
296 w.write_all(line)?;
297 self.at_line_start = line.ends_with(b"\n");
298 }
299 w.flush()
300 }
301
302 fn finish(&mut self, w: &mut impl Write) -> io::Result<()> {
304 if self.prefix.is_some() && !self.at_line_start {
305 self.at_line_start = true;
306 w.write_all(b"\n")?;
307 return w.flush();
308 }
309 Ok(())
310 }
311}
312
313fn ancestors() -> Vec<(u32, u64)> {
316 let mut out = Vec::new();
317 let Some((mut ppid, _)) = stat(std::process::id()) else {
318 return out;
319 };
320 while ppid > 1 && out.len() < 64 {
321 let Some((next, start)) = stat(ppid) else {
322 break;
323 };
324 out.push((ppid, start));
325 ppid = next;
326 }
327 out
328}
329
330fn alive(pid: u32, start: u64) -> bool {
331 matches!(stat(pid), Some((_, s)) if s == start)
332}
333
334#[cfg(target_os = "linux")]
337fn stat(pid: u32) -> Option<(u32, u64)> {
338 let s = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
339 parse_stat(&s)
340}
341
342#[cfg(target_os = "macos")]
345fn stat(pid: u32) -> Option<(u32, u64)> {
346 let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
347 let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
348 let n = unsafe {
350 libc::proc_pidinfo(
351 libc::pid_t::try_from(pid).ok()?,
352 libc::PROC_PIDTBSDINFO,
353 0,
354 info.as_mut_ptr().cast(),
355 size,
356 )
357 };
358 if n != size {
359 return None;
360 }
361 let info = unsafe { info.assume_init() };
363 if info.pbi_status == libc::SZOMB {
364 return None;
365 }
366 Some((
367 info.pbi_ppid,
368 info.pbi_start_tvsec * 1_000_000 + info.pbi_start_tvusec,
369 ))
370}
371
372#[cfg(target_os = "linux")]
373fn parse_stat(s: &str) -> Option<(u32, u64)> {
374 let rest = &s[s.rfind(')')? + 1..];
377 let f: Vec<&str> = rest.split_whitespace().collect();
378 if matches!(f.first(), Some(&"Z") | Some(&"X")) {
379 return None;
380 }
381 Some((f.get(1)?.parse().ok()?, f.get(19)?.parse().ok()?))
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[cfg(target_os = "linux")]
389 #[test]
390 fn parses_stat() {
391 let line =
392 "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";
393 assert_eq!(parse_stat(line), Some((99, 5555)));
394 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";
395 assert_eq!(parse_stat(zombie), None);
396 }
397
398 #[test]
399 fn own_ancestors_are_alive() {
400 let a = ancestors();
401 assert!(!a.is_empty());
402 assert!(a.iter().all(|&(p, t)| alive(p, t)));
403 assert!(!alive(a[0].0, a[0].1 + 1));
404 }
405
406 #[test]
407 fn prefixes_every_line() {
408 let mut w = LineWriter::new(Some("web | ".into()));
409 let mut out = Vec::new();
410 w.write(&mut out, b"one\ntw").unwrap();
411 w.write(&mut out, b"o\nthree").unwrap();
412 w.finish(&mut out).unwrap();
413 assert_eq!(out, b"web | one\nweb | two\nweb | three\n");
414 }
415}