tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1569Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ main crates/hammerd/src/control.rs Rust · 141 siq'ikuna · 5.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja b818f5249 · Sergio ·

1//! `/run/init.control` — FIFO de control humano. Ver `docs/07-agent-bus.md` §1.
2//!
3//! Modelo Fase 5: el FIFO existe y acepta escrituras (`echo "start web" > …`); las líneas
4//! recibidas se loguean por ahora. La supervisión real de servicios llega cuando entre el
5//! init propio del track posterior (`/etc/service/*/run` estilo s6/runit).
6//!
7//! Por qué un FIFO y no un socket: es lo MÁS unix posible (`cat`, `echo`, redirección),
8//! ergonomía máxima sin parser ni framing. Para programas serios está el `/run/agent.sock`.
9
10use std::io::{BufRead, BufReader, Write};
11use std::path::{Path, PathBuf};
12
13/// Crea (o reusa) el FIFO en `path` y devuelve la ruta canónica. Si el path existe y NO es
14/// un FIFO falla con mensaje claro: no queremos pisar archivos por accidente.
15pub fn ensure_fifo(path: &Path) -> std::io::Result<PathBuf> {
16    use std::os::unix::fs::FileTypeExt;
17    if let Some(parent) = path.parent() {
18        std::fs::create_dir_all(parent)?;
19    }
20    match std::fs::symlink_metadata(path) {
21        Ok(m) if m.file_type().is_fifo() => return Ok(path.to_path_buf()),
22        Ok(_) => {
23            return Err(std::io::Error::new(
24                std::io::ErrorKind::AlreadyExists,
25                format!(
26                    "init-control: {} existe y NO es un FIFO; mueve o borra el archivo",
27                    path.display()
28                ),
29            ));
30        }
31        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
32        Err(e) => return Err(e),
33    }
34    // `nix::unistd::mkfifo` con permisos 0o660: owner + group escriben/leen, world nada.
35    // El humano normalmente está en el grupo del daemon (p. ej. `takana`); la IA agente
36    // entra por el socket.
37    use nix::sys::stat::Mode;
38    nix::unistd::mkfifo(path, Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IWGRP)
39        .map_err(std::io::Error::from)?;
40    Ok(path.to_path_buf())
41}
42
43/// Lazo bloqueante: abre el FIFO en modo lectura y loguea cada línea. Cuando el último
44/// escritor cierra, `read_line` devuelve 0 (EOF); volvemos a abrir para no salir del lazo.
45///
46/// Pensado para correr en su propio thread desde `main`.
47pub fn run_reader(path: &Path) -> std::io::Result<()> {
48    loop {
49        let f = match std::fs::OpenOptions::new().read(true).open(path) {
50            Ok(f) => f,
51            Err(e) => {
52                tracing::warn!(error = %e, "init-control: no pude abrir para lectura");
53                return Err(e);
54            }
55        };
56        let mut reader = BufReader::new(f);
57        loop {
58            let mut line = String::new();
59            match reader.read_line(&mut line) {
60                Ok(0) => break, // EOF: re-abrimos
61                Ok(_) => {
62                    let trimmed = line.trim_end_matches('\n');
63                    tracing::info!(line = %trimmed, "init-control");
64                }
65                Err(e) => {
66                    tracing::warn!(error = %e, "init-control: error de lectura");
67                    break;
68                }
69            }
70        }
71    }
72}
73
74/// Envía una línea al FIFO. Si nadie está leyendo, **bloquea** hasta que el reader la
75/// consuma: ése es exactamente el contrato de un FIFO sin O_NONBLOCK, y la garantía que el
76/// agente espera (`InitAck` significa "la línea entró al canal").
77pub fn send_to_fifo(path: &Path, line: &str) -> std::io::Result<()> {
78    let mut f = std::fs::OpenOptions::new().write(true).open(path)?;
79    f.write_all(line.as_bytes())?;
80    if !line.ends_with('\n') {
81        f.write_all(b"\n")?;
82    }
83    Ok(())
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn ensure_fifo_creates_when_missing() {
92        let d = tempfile::tempdir().unwrap();
93        let p = d.path().join("init.ctl");
94        ensure_fifo(&p).unwrap();
95        use std::os::unix::fs::FileTypeExt;
96        assert!(p.symlink_metadata().unwrap().file_type().is_fifo());
97    }
98
99    #[test]
100    fn ensure_fifo_idempotent() {
101        let d = tempfile::tempdir().unwrap();
102        let p = d.path().join("init.ctl");
103        ensure_fifo(&p).unwrap();
104        ensure_fifo(&p).unwrap(); // sin error: ya es FIFO
105    }
106
107    #[test]
108    fn ensure_fifo_rejects_non_fifo() {
109        let d = tempfile::tempdir().unwrap();
110        let p = d.path().join("not-a-fifo");
111        std::fs::write(&p, b"x").unwrap();
112        let err = ensure_fifo(&p).unwrap_err().to_string();
113        assert!(err.contains("NO es un FIFO"), "{err}");
114    }
115
116    #[test]
117    fn send_and_read_one_line() {
118        let d = tempfile::tempdir().unwrap();
119        let p = d.path().join("init.ctl");
120        ensure_fifo(&p).unwrap();
121
122        // Reader en background; sólo lee una línea y termina.
123        let reader_path = p.clone();
124        let reader = std::thread::spawn(move || {
125            let f = std::fs::OpenOptions::new()
126                .read(true)
127                .open(&reader_path)
128                .unwrap();
129            let mut r = BufReader::new(f);
130            let mut s = String::new();
131            r.read_line(&mut s).unwrap();
132            s
133        });
134
135        // Pequeño retraso para asegurar que el reader llega a `open` antes del writer.
136        std::thread::sleep(std::time::Duration::from_millis(50));
137        send_to_fifo(&p, "start web").unwrap();
138        let got = reader.join().unwrap();
139        assert_eq!(got, "start web\n");
140    }
141}