⎇ 762bf95dfd770cc8bf51f21605bc2cc6cc88ea76
crates/init/arje-absorb/src/runit.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
1//! Absorbe **runit**: descubre los servicios bajo supervisión.
2
3use std::fs;
4use std::path::Path;
5
6use crate::model::{ForeignService, ServiceKind};
7
8/// Descubre los servicios runit de `<root>`.
9///
10/// runit supervisa un directorio de servicios activos (el `runsvdir`
11/// del runlevel). Cada entrada apunta a `/etc/sv/<nombre>`, cuyo script
12/// `run` es un daemon en primer plano — el calce con arje es 1:1: arje
13/// supervisa ese mismo `run`. Si no hay `runsvdir`, cae a `/etc/sv`
14/// (todos los servicios definidos).
15pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
16 let runsvdir = [
17 "etc/runit/runsvdir/default",
18 "etc/runit/runsvdir/current",
19 "service",
20 "var/service",
21 "etc/service",
22 ]
23 .into_iter()
24 .map(|c| root.join(c))
25 .find(|p| p.is_dir());
26
27 let scan = match runsvdir {
28 Some(d) => d,
29 None => {
30 let sv = root.join("etc/sv");
31 anyhow::ensure!(
32 sv.is_dir(),
33 "no encontré servicios runit en {}",
34 root.display()
35 );
36 sv
37 }
38 };
39
40 let mut names: Vec<String> = fs::read_dir(&scan)?
41 .flatten()
42 .map(|e| e.file_name().to_string_lossy().into_owned())
43 .filter(|n| !n.starts_with('.'))
44 .collect();
45 names.sort();
46
47 let mut out = Vec::new();
48 for name in names {
49 // El servicio vive en /etc/sv/<name>; su run-script es el exec.
50 let run = root.join("etc/sv").join(&name).join("run");
51 if !run.exists() {
52 continue; // entrada sin run-script real — la saltamos
53 }
54 out.push(ForeignService {
55 name: format!("runit-{name}"),
56 exec: format!("/etc/sv/{name}/run"),
57 argv: Vec::new(),
58 env: Vec::new(),
59 kind: ServiceKind::Daemon, // runit siempre supervisa
60 });
61 }
62 Ok(out)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 fn make_sv(root: &Path, name: &str) {
70 let d = root.join("etc/sv").join(name);
71 fs::create_dir_all(&d).unwrap();
72 fs::write(d.join("run"), "#!/bin/sh\nexec daemon\n").unwrap();
73 }
74
75 #[test]
76 fn absorbs_from_etc_sv_fallback() {
77 let tmp = tempfile::tempdir().unwrap();
78 make_sv(tmp.path(), "sshd");
79 make_sv(tmp.path(), "dhcpcd");
80 let svcs = absorb(tmp.path()).unwrap();
81 assert_eq!(svcs.len(), 2);
82 // Salida ordenada por nombre.
83 assert_eq!(svcs[0].name, "runit-dhcpcd");
84 assert_eq!(svcs[0].exec, "/etc/sv/dhcpcd/run");
85 assert_eq!(svcs[0].kind, ServiceKind::Daemon);
86 }
87
88 #[test]
89 fn absorbs_only_enabled_from_runsvdir() {
90 let tmp = tempfile::tempdir().unwrap();
91 for s in ["sshd", "dhcpcd", "apagado"] {
92 make_sv(tmp.path(), s);
93 }
94 let rsv = tmp.path().join("etc/runit/runsvdir/default");
95 fs::create_dir_all(&rsv).unwrap();
96 // Sólo sshd y dhcpcd están habilitados (symlink en el runsvdir).
97 for s in ["sshd", "dhcpcd"] {
98 std::os::unix::fs::symlink(tmp.path().join("etc/sv").join(s), rsv.join(s))
99 .unwrap();
100 }
101 let svcs = absorb(tmp.path()).unwrap();
102 assert_eq!(svcs.len(), 2);
103 assert!(svcs.iter().all(|s| s.name != "runit-apagado"));
104 }
105
106 #[test]
107 fn errors_without_services() {
108 let tmp = tempfile::tempdir().unwrap();
109 assert!(absorb(tmp.path()).is_err());
110 }
111}