⎇ 762bf95dfd770cc8bf51f21605bc2cc6cc88ea76
crates/init/arje-absorb/src/sysvinit.rs
Historia
Quién tocó qué
crudo
feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
1//! Absorbe **sysvinit**: parsea `/etc/inittab`.
2
3use std::fs;
4use std::path::Path;
5
6use anyhow::Context;
7
8use crate::model::{split_command, ForeignService, ServiceKind};
9
10/// Lee `<root>/etc/inittab` y devuelve sus servicios.
11///
12/// Formato de cada línea: `id:runlevels:action:process`. Tomamos las
13/// que tienen un `process` real: `respawn` → daemon supervisado;
14/// `wait`/`once`/`boot`/`bootwait`/`sysinit` → one-shot. El resto de
15/// acciones (`initdefault`, `ctrlaltdel`, `power*`, `off`, …) no lanzan
16/// un servicio y se ignoran.
17pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
18 let path = root.join("etc/inittab");
19 let text =
20 fs::read_to_string(&path).with_context(|| format!("leyendo {}", path.display()))?;
21 let mut out = Vec::new();
22 for raw in text.lines() {
23 let line = raw.trim();
24 if line.is_empty() || line.starts_with('#') {
25 continue;
26 }
27 let mut f = line.splitn(4, ':');
28 let (id, action, process) = match (f.next(), f.next(), f.next(), f.next()) {
29 (Some(id), Some(_rl), Some(action), Some(process)) => (id, action, process),
30 _ => continue, // línea malformada
31 };
32 let kind = match action.trim() {
33 "respawn" => ServiceKind::Daemon,
34 "wait" | "once" | "boot" | "bootwait" | "sysinit" => ServiceKind::OneShot,
35 _ => continue,
36 };
37 // El proceso puede empezar con `+` (sysvinit: no escribir utmp).
38 let process = process.trim().trim_start_matches('+').trim();
39 let Some((exec, argv)) = split_command(process) else {
40 continue;
41 };
42 out.push(ForeignService {
43 name: format!("sysv-{}", id.trim()),
44 exec,
45 argv,
46 env: Vec::new(),
47 kind,
48 });
49 }
50 Ok(out)
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 fn with_inittab(content: &str) -> tempfile::TempDir {
58 let tmp = tempfile::tempdir().unwrap();
59 fs::create_dir_all(tmp.path().join("etc")).unwrap();
60 fs::write(tmp.path().join("etc/inittab"), content).unwrap();
61 tmp
62 }
63
64 #[test]
65 fn parses_respawn_and_oneshot() {
66 let tmp = with_inittab(
67 "# consolas del sistema\n\
68 id:3:initdefault:\n\
69 si::sysinit:/etc/rc.d/rc.sysinit\n\
70 1:2345:respawn:/sbin/agetty 38400 tty1 linux\n\
71 rc::wait:/etc/rc.d/rc 3\n",
72 );
73 let svcs = absorb(tmp.path()).unwrap();
74 // sysinit + respawn + wait — initdefault no cuenta.
75 assert_eq!(svcs.len(), 3);
76 let agetty = svcs.iter().find(|s| s.name == "sysv-1").unwrap();
77 assert_eq!(agetty.kind, ServiceKind::Daemon);
78 assert_eq!(agetty.exec, "/sbin/agetty");
79 assert_eq!(agetty.argv, ["38400", "tty1", "linux"]);
80 let si = svcs.iter().find(|s| s.name == "sysv-si").unwrap();
81 assert_eq!(si.kind, ServiceKind::OneShot);
82 }
83
84 #[test]
85 fn skips_comments_and_blank_lines() {
86 let tmp = with_inittab("\n \n# sólo comentarios\n");
87 assert!(absorb(tmp.path()).unwrap().is_empty());
88 }
89
90 #[test]
91 fn strips_leading_plus() {
92 let tmp = with_inittab("x:2:respawn:+/sbin/getty tty2\n");
93 let svcs = absorb(tmp.path()).unwrap();
94 assert_eq!(svcs[0].exec, "/sbin/getty");
95 }
96}