1use std::collections::HashMap;
9use thiserror::Error;
10
11use crate::config::{InitConfig, RestartPolicy, Service};
12
13pub type Pid = i32;
14
15#[derive(Debug, Error)]
16pub enum SupervisorError {
17 #[error("failed to spawn {name}: {reason}")]
18 Spawn { name: String, reason: String },
19
20 #[error("failed to signal pid {pid}: {reason}")]
21 Signal { pid: Pid, reason: String },
22
23 #[error("io: {0}")]
24 Io(#[from] std::io::Error),
25}
26
27pub type Result<T> = std::result::Result<T, SupervisorError>;
28
29pub trait Supervisor {
35 fn spawn(&mut self, svc: &Service) -> Result<Pid>;
37
38 fn terminate(&mut self, pid: Pid) -> Result<()>;
40
41 fn kill(&mut self, pid: Pid) -> Result<()>;
43
44 fn reap_one(&mut self) -> Result<Option<(Pid, i32)>>;
47
48 fn children(&self) -> Vec<(Pid, String)>;
50}
51
52#[derive(Default)]
57pub struct MockSupervisor {
58 next_pid: Pid,
59 live: HashMap<Pid, String>,
60 pub queued_exits: Vec<(Pid, i32)>,
63 pub log: Vec<String>,
65}
66
67impl MockSupervisor {
68 pub fn new() -> Self {
69 Self {
70 next_pid: 100,
71 ..Default::default()
72 }
73 }
74
75 pub fn queue_exit(&mut self, pid: Pid, status: i32) {
77 self.queued_exits.push((pid, status));
78 }
79}
80
81impl Supervisor for MockSupervisor {
82 fn spawn(&mut self, svc: &Service) -> Result<Pid> {
83 let pid = self.next_pid;
84 self.next_pid += 1;
85 self.live.insert(pid, svc.name.clone());
86 self.log.push(format!("spawn {} ({})", svc.name, pid));
87 Ok(pid)
88 }
89
90 fn terminate(&mut self, pid: Pid) -> Result<()> {
91 self.log.push(format!("SIGTERM {pid}"));
92 Ok(())
93 }
94
95 fn kill(&mut self, pid: Pid) -> Result<()> {
96 self.log.push(format!("SIGKILL {pid}"));
97 self.live.remove(&pid);
98 Ok(())
99 }
100
101 fn reap_one(&mut self) -> Result<Option<(Pid, i32)>> {
102 if let Some((pid, status)) = self.queued_exits.pop() {
103 let name = self.live.remove(&pid).unwrap_or_else(|| "?".into());
104 self.log.push(format!("reap {name} ({pid}) -> {status}"));
105 Ok(Some((pid, status)))
106 } else {
107 Ok(None)
108 }
109 }
110
111 fn children(&self) -> Vec<(Pid, String)> {
112 self.live.iter().map(|(p, n)| (*p, n.clone())).collect()
113 }
114}
115
116pub struct LinuxSupervisor {
122 live: HashMap<Pid, String>,
123}
124
125impl Default for LinuxSupervisor {
126 fn default() -> Self {
127 Self::new()
128 }
129}
130
131impl LinuxSupervisor {
132 pub fn new() -> Self {
133 Self {
134 live: HashMap::new(),
135 }
136 }
137}
138
139#[cfg(unix)]
140impl Supervisor for LinuxSupervisor {
141 fn spawn(&mut self, svc: &Service) -> Result<Pid> {
142 use std::ffi::CString;
143 let argv: Vec<String> = split_exec(&svc.resolved_exec());
146 if argv.is_empty() {
147 return Err(SupervisorError::Spawn {
148 name: svc.name.clone(),
149 reason: "empty exec line".into(),
150 });
151 }
152 let argv_c: Vec<CString> = argv
153 .iter()
154 .map(|s| CString::new(s.as_str()).unwrap_or_default())
155 .collect();
156 let cwd_c = svc.workdir.as_deref().and_then(|c| CString::new(c).ok());
157 let env_c: Vec<(CString, CString)> = svc
158 .env
159 .iter()
160 .filter_map(|(k, v)| {
161 Some((
162 CString::new(k.as_str()).ok()?,
163 CString::new(v.as_str()).ok()?,
164 ))
165 })
166 .collect();
167
168 let pid = unsafe { libc::fork() };
170 if pid < 0 {
171 return Err(SupervisorError::Spawn {
172 name: svc.name.clone(),
173 reason: std::io::Error::last_os_error().to_string(),
174 });
175 }
176 if pid == 0 {
177 if let Some(c) = cwd_c.as_ref() {
180 unsafe { libc::chdir(c.as_ptr()) };
181 }
182 for (k, v) in &env_c {
183 unsafe { libc::setenv(k.as_ptr(), v.as_ptr(), 1) };
184 }
185 let mut argv_ptr: Vec<*const libc::c_char> =
186 argv_c.iter().map(|s| s.as_ptr()).collect();
187 argv_ptr.push(std::ptr::null());
188 unsafe { libc::execvp(argv_c[0].as_ptr(), argv_ptr.as_ptr()) };
189 unsafe { libc::_exit(127) };
191 }
192
193 self.live.insert(pid, svc.name.clone());
194 Ok(pid)
195 }
196
197 fn terminate(&mut self, pid: Pid) -> Result<()> {
198 send_signal(pid, libc::SIGTERM)
199 }
200
201 fn kill(&mut self, pid: Pid) -> Result<()> {
202 let out = send_signal(pid, libc::SIGKILL);
203 self.live.remove(&pid);
204 out
205 }
206
207 fn reap_one(&mut self) -> Result<Option<(Pid, i32)>> {
208 let mut status: libc::c_int = 0;
209 let r = unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) };
212 if r == 0 {
213 return Ok(None);
214 }
215 if r < 0 {
216 let e = std::io::Error::last_os_error();
217 if e.raw_os_error() == Some(libc::ECHILD) {
218 return Ok(None);
219 }
220 return Err(SupervisorError::Io(e));
221 }
222 self.live.remove(&r);
223 let exit_code = if libc::WIFEXITED(status) {
226 libc::WEXITSTATUS(status)
227 } else {
228 128 + libc::WTERMSIG(status)
229 };
230 Ok(Some((r, exit_code)))
231 }
232
233 fn children(&self) -> Vec<(Pid, String)> {
234 self.live.iter().map(|(p, n)| (*p, n.clone())).collect()
235 }
236}
237
238#[cfg(not(unix))]
239impl Supervisor for LinuxSupervisor {
240 fn spawn(&mut self, _svc: &Service) -> Result<Pid> {
241 Err(SupervisorError::Spawn {
242 name: "n/a".into(),
243 reason: "LinuxSupervisor requires a Unix host".into(),
244 })
245 }
246 fn terminate(&mut self, _pid: Pid) -> Result<()> {
247 Ok(())
248 }
249 fn kill(&mut self, _pid: Pid) -> Result<()> {
250 Ok(())
251 }
252 fn reap_one(&mut self) -> Result<Option<(Pid, i32)>> {
253 Ok(None)
254 }
255 fn children(&self) -> Vec<(Pid, String)> {
256 vec![]
257 }
258}
259
260#[cfg(unix)]
261fn send_signal(pid: Pid, sig: libc::c_int) -> Result<()> {
262 let r = unsafe { libc::kill(pid, sig) };
264 if r != 0 {
265 let e = std::io::Error::last_os_error();
266 if e.raw_os_error() == Some(libc::ESRCH) {
267 return Ok(()); }
269 return Err(SupervisorError::Signal {
270 pid,
271 reason: e.to_string(),
272 });
273 }
274 Ok(())
275}
276
277#[cfg(not(unix))]
278fn send_signal(_pid: Pid, _sig: i32) -> Result<()> {
279 Ok(())
280}
281
282pub fn boot<S: Supervisor>(sup: &mut S, cfg: &InitConfig) -> Result<HashMap<String, Pid>> {
288 let mut by_name = HashMap::new();
289 for svc in &cfg.services {
290 if !svc.enable {
291 continue;
292 }
293 let pid = sup.spawn(svc)?;
294 by_name.insert(svc.name.clone(), pid);
295 }
296 Ok(by_name)
297}
298
299pub fn run_once<S: Supervisor>(
302 sup: &mut S,
303 cfg: &InitConfig,
304 tracking: &mut HashMap<Pid, String>,
305) -> Result<Vec<ReapedEvent>> {
306 let mut events = Vec::new();
307 while let Some((pid, status)) = sup.reap_one()? {
308 let name = tracking.remove(&pid);
309 let svc = name
310 .as_deref()
311 .and_then(|n| cfg.services.iter().find(|s| s.name == n));
312 let restart = svc.map(|s| s.restart).unwrap_or(RestartPolicy::Never);
313 let should_restart = match (restart, status) {
314 (RestartPolicy::Always, _) => true,
315 (RestartPolicy::OnFailure, 0) => false,
316 (RestartPolicy::OnFailure, _) => true,
317 (RestartPolicy::Never, _) => false,
318 };
319 let new_pid = if should_restart {
320 if let Some(s) = svc {
321 let new_pid = sup.spawn(s)?;
322 tracking.insert(new_pid, s.name.clone());
323 Some(new_pid)
324 } else {
325 None
326 }
327 } else {
328 None
329 };
330 events.push(ReapedEvent {
331 pid,
332 name: name.unwrap_or_else(|| "unknown".into()),
333 exit_status: status,
334 restarted_as: new_pid,
335 });
336 }
337 Ok(events)
338}
339
340#[derive(Debug, Clone)]
341pub struct ReapedEvent {
342 pub pid: Pid,
343 pub name: String,
344 pub exit_status: i32,
345 pub restarted_as: Option<Pid>,
346}
347
348fn split_exec(cmd: &str) -> Vec<String> {
351 cmd.split_whitespace().map(String::from).collect()
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::config::{RestartPolicy, Service};
358
359 fn svc(name: &str, exec: &str, restart: RestartPolicy, enable: bool) -> Service {
360 Service {
361 name: name.into(),
362 exec: exec.into(),
363 body: None,
364 restart,
365 env: vec![],
366 workdir: None,
367 enable,
368 }
369 }
370
371 #[test]
372 fn boot_spawns_every_enabled_service() {
373 let mut sup = MockSupervisor::new();
374 let cfg = InitConfig {
375 services: vec![
376 svc("a", "/a", RestartPolicy::Never, true),
377 svc("b", "/b", RestartPolicy::Never, false), svc("c", "/c", RestartPolicy::Never, true),
379 ],
380 ..Default::default()
381 };
382 let by_name = boot(&mut sup, &cfg).unwrap();
383 assert_eq!(by_name.len(), 2);
384 assert!(by_name.contains_key("a"));
385 assert!(by_name.contains_key("c"));
386 assert_eq!(sup.children().len(), 2);
387 }
388
389 #[test]
390 fn restart_policy_always_respawns_on_zero_exit() {
391 let mut sup = MockSupervisor::new();
392 let cfg = InitConfig {
393 services: vec![svc("daemon", "/daemon", RestartPolicy::Always, true)],
394 ..Default::default()
395 };
396 let by_name = boot(&mut sup, &cfg).unwrap();
397 let mut tracking: HashMap<Pid, String> =
398 by_name.iter().map(|(n, p)| (*p, n.clone())).collect();
399 sup.queue_exit(tracking.keys().next().copied().unwrap(), 0);
400 let events = run_once(&mut sup, &cfg, &mut tracking).unwrap();
401 assert_eq!(events.len(), 1);
402 assert!(events[0].restarted_as.is_some());
403 assert_eq!(sup.children().len(), 1);
404 }
405
406 #[test]
407 fn restart_policy_on_failure_respawns_only_on_nonzero() {
408 let mut sup = MockSupervisor::new();
409 let cfg = InitConfig {
410 services: vec![svc("job", "/job", RestartPolicy::OnFailure, true)],
411 ..Default::default()
412 };
413 let by_name = boot(&mut sup, &cfg).unwrap();
414 let mut tracking: HashMap<Pid, String> =
415 by_name.iter().map(|(n, p)| (*p, n.clone())).collect();
416 let pid = *tracking.keys().next().unwrap();
417 sup.queue_exit(pid, 0);
418 let ev = run_once(&mut sup, &cfg, &mut tracking).unwrap();
419 assert_eq!(ev.len(), 1);
420 assert!(ev[0].restarted_as.is_none());
421 assert_eq!(sup.children().len(), 0);
422 }
423
424 #[test]
425 fn restart_policy_never_never_respawns() {
426 let mut sup = MockSupervisor::new();
427 let cfg = InitConfig {
428 services: vec![svc("oneshot", "/oneshot", RestartPolicy::Never, true)],
429 ..Default::default()
430 };
431 let by_name = boot(&mut sup, &cfg).unwrap();
432 let mut tracking: HashMap<Pid, String> =
433 by_name.iter().map(|(n, p)| (*p, n.clone())).collect();
434 sup.queue_exit(*tracking.keys().next().unwrap(), 1);
435 let ev = run_once(&mut sup, &cfg, &mut tracking).unwrap();
436 assert_eq!(ev.len(), 1);
437 assert!(ev[0].restarted_as.is_none());
438 }
439
440 #[test]
441 fn reap_one_returns_none_when_nothing_queued() {
442 let mut sup = MockSupervisor::new();
443 assert!(sup.reap_one().unwrap().is_none());
444 }
445
446 #[test]
447 fn mock_logs_are_sequential_and_useful() {
448 let mut sup = MockSupervisor::new();
449 let s = svc("x", "/x", RestartPolicy::Never, true);
450 sup.spawn(&s).unwrap();
451 let children = sup.children();
452 assert_eq!(children.len(), 1);
453 let pid = children[0].0;
454 sup.terminate(pid).unwrap();
455 sup.queue_exit(pid, 0);
456 sup.reap_one().unwrap();
457 assert_eq!(sup.log[0], "spawn x (100)");
458 assert_eq!(sup.log[1], "SIGTERM 100");
459 assert_eq!(sup.log[2], "reap x (100) -> 0");
460 }
461}