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…
takana etapa 5d: comentarios de crates, CLAUDE.md y el skill de granja
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}