feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
1//! El modelo intermedio: un servicio descubierto en el init ajeno,
2//! independiente de su formato de origen. Cada absorber
3//! (`sysvinit`, `runit`, `dinit`, `openrc`) produce [`ForeignService`]s;
4//! `card` los traduce a Cards brahman.
5
6/// Un servicio leído de la configuración de otro init.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ForeignService {
9 /// Etiqueta del servicio — pasa a ser el `label` de su Card.
10 pub name: String,
11 /// Ejecutable (path absoluto, tal como existirá en el sistema).
12 pub exec: String,
13 /// Argumentos del ejecutable.
14 pub argv: Vec<String>,
15 /// Variables de entorno (clave, valor) a inyectarle.
16 pub env: Vec<(String, String)>,
17 /// Daemon supervisado, o tarea de una sola ejecución.
18 pub kind: ServiceKind,
19}
20
21/// Cómo trata arje a un servicio absorbido.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ServiceKind {
24 /// Servicio de larga duración: arje lo supervisa y reinicia.
25 Daemon,
26 /// Tarea puntual: corre una vez y termina.
27 OneShot,
28}
29
30/// Parte una línea de comando en `(exec, argv)`, respetando comillas
31/// simples y dobles. Best-effort: no expande variables ni globs —
32/// suficiente para los campos `process`/`command` de los inits
33/// clásicos. `None` si la línea no tiene ningún token.
34pub fn split_command(line: &str) -> Option<(String, Vec<String>)> {
35 let mut tokens: Vec<String> = Vec::new();
36 let mut cur = String::new();
37 let mut in_token = false;
38 let mut chars = line.chars().peekable();
39 while let Some(c) = chars.next() {
40 match c {
41 '"' | '\'' => {
42 in_token = true;
43 let quote = c;
44 for q in chars.by_ref() {
45 if q == quote {
46 break;
47 }
48 cur.push(q);
49 }
50 }
51 c if c.is_whitespace() => {
52 if in_token {
53 tokens.push(std::mem::take(&mut cur));
54 in_token = false;
55 }
56 }
57 c => {
58 in_token = true;
59 cur.push(c);
60 }
61 }
62 }
63 if in_token {
64 tokens.push(cur);
65 }
66 if tokens.is_empty() {
67 return None;
68 }
69 let exec = tokens.remove(0);
70 Some((exec, tokens))
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn splits_plain_command() {
79 let (exec, argv) = split_command("/usr/sbin/sshd -D -e").unwrap();
80 assert_eq!(exec, "/usr/sbin/sshd");
81 assert_eq!(argv, ["-D", "-e"]);
82 }
83
84 #[test]
85 fn respects_double_quotes() {
86 let (exec, argv) = split_command(r#"/bin/foo "arg con espacios" bar"#).unwrap();
87 assert_eq!(exec, "/bin/foo");
88 assert_eq!(argv, ["arg con espacios", "bar"]);
89 }
90
91 #[test]
92 fn respects_single_quotes() {
93 let (exec, argv) = split_command("/bin/sh -c 'echo hola mundo'").unwrap();
94 assert_eq!(exec, "/bin/sh");
95 assert_eq!(argv, ["-c", "echo hola mundo"]);
96 }
97
98 #[test]
99 fn empty_line_yields_none() {
100 assert!(split_command(" ").is_none());
101 assert!(split_command("").is_none());
102 }
103}