//! `[[service]]` — **el servicio que un paquete TRAE**, traducido a una Card de arje. //! //! ## Por qué existe (el hueco, medido) //! //! Hasta hoy una receta no tenía dónde decir «yo traigo un demonio». La consecuencia no era //! teórica: los dos únicos servicios del producto (`hammerd` y `sshd`) vivían **hardcodeados en //! constantes de Rust** (`takana_bootstrap::SSHD_SERVICE_CARD`), y los ocho demonios de una sesión //! GNOME (dbus, logind-compat, accounts-daemon, upowerd, pipewire, pipewire-pulse, wireplumber, //! colord) se lanzaban a mano con `&` desde `scripts/gnome/gnome-start-qemu.sh` — **sin supervisión, //! sin backoff y sin el `CRASHED` real**, que es justo lo que arje sabe hacer y para lo que se lo //! adoptó como PID 1 (SDD 12). //! //! ## Por qué NO alcanza con `arje-absorb` //! //! arje ya trae el traductor de inits ajenos (`init/arje-absorb`, con lectores de systemd, OpenRC, //! runit, dinit y sysvinit). No sirve acá, y la razón es exacta: **`arje-absorb` sólo absorbe lo //! que está HABILITADO** — los symlinks de `.wants/` — porque su trabajo es *retratar* un //! sistema vivo, no inventarle uno. Un rootfs de takana no tiene ese estado: medido el 2026-09-12 //! sobre el store, hay **62 ficheros `.service` sellados (61 de usuario, 1 de sistema) y CERO //! directorios `.wants`**. Absorber un rootfs nuestro devuelve la lista vacía, y es la respuesta //! correcta a la pregunta que `arje-absorb` contesta. //! //! O sea que el «enable» de una distro construida desde fuente **no puede leerse del árbol: hay que //! declararlo**. Esa declaración se parte en dos, igual que systemd parte `[Service]` de //! `[Install]`: //! //! - **qué es el servicio** (exec, argv, supervisión) — propiedad del PAQUETE ⇒ este bloque; //! - **si arranca en esta imagen** — propiedad del PERFIL ⇒ `docs/state/targets.toml` (pendiente). //! //! ## Fuera de `hash_inputs` //! //! Igual que `license`, `slots` y `evidence`: describe **cómo se supervisa** el artefacto, no qué //! bytes tiene. `Recipe::hash_inputs` es una lista explícita de entradas, así que declarar servicios //! en recetas YA SELLADAS no mueve un solo `ArtifactHash` — que es lo único que hace la deuda //! pagable sin reconstruir el corpus. //! //! ## Límite conocido: no hay readiness //! //! arje ordena el arranque por **capacidades** (`resolve::plan_spawn`, orden topológico), pero eso //! ordena el *spawn*, no el *estar listo*: no existe primitiva «esperá a que aparezca este socket» //! (`DE-SYSTEMD-A-ARJE.md` §13 lo lista como hueco abierto — el `READY=1` de `Type=notify` se //! registra y no ordena). Mientras no exista, un servicio que necesita esperar a otro lo hace //! DENTRO de su propio card, con un `sh -c '…; exec …'` — que es exactamente lo que ya hace el card //! de `sshd` (levanta la red y genera las host keys antes del `exec`). Este módulo no lo esconde: //! lo deja explícito en `argv`. use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; /// Un servicio declarado por una receta. Se traduce 1:1 a una Card de arje ([`Service::card`]). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Service { /// Nombre de la unidad (`sshd`). Es el `label` de la Card y la clave con la que `arjectl` la /// nombra. Sin `/`: también es nombre de fichero en `cards.d/`. pub label: String, /// ULID de la Card. **Se declara, no se genera**: un id aleatorio haría que la seed —y con ella /// el `product_rootfs_hash`— cambiara en cada corrida, rompiendo la reproducibilidad del /// producto. Que sea estable es un requisito, no un detalle. pub id: String, /// El ELF a encarnar (`Payload::Native.exec`). Absoluto: lo ejecuta PID 1, que no tiene `PATH`. pub exec: String, #[serde(default)] pub argv: Vec, /// Entorno explícito. arje inyecta además `ENTE_BUS_SOCK` y `ENTE_ID` por su cuenta. #[serde(default)] pub envp: Vec<(String, String)>, /// `daemon` (default) u `oneshot` — el `lifecycle` de la Card. #[serde(default)] pub kind: Kind, /// **De sistema o de sesión** — y no es cosmético: decide DÓNDE va la Card. /// /// `system` (default) = arranca al boot, va en el `genesis` de la seed. `session` = pertenece a /// una sesión gráfica (pipewire, wireplumber, el bus de sesión): necesita `XDG_RUNTIME_DIR` y un /// usuario logueado, así que meterla en el genesis la arrancaría antes de que exista ninguna de /// las dos cosas. /// /// El campo existe porque el destino de una Card de sesión **hoy no está resuelto fuera de /// mirada**: ahí lo hace el compositor (`mirada-compositor/src/session.rs` arma /// `session.{label}` con `requires = [wayland_floor()]` y la entrega a PID 1 por `RunCard`), /// pero mutter y kwin no tienen esa integración y sus demonios de sesión los sigue lanzando el /// script de arranque. Declararlo igual hace que el hueco se CUENTE en vez de omitirse — /// `scripts/targets.py --servicios` avisa por cada uno. #[serde(default)] pub scope: Scope, /// Política de supervisión. **Ausente ⇒ `OneShot`** (no revive). Presente ⇒ `Restart` con /// backoff exponencial entre `initial_ms` y `max_ms`, y su muerte es el `CRASHED` real. #[serde(default, skip_serializing_if = "Option::is_none")] pub restart: Option, /// Permiso de red del Ente. Default `none`: un demonio que no lo declara no toca la red. #[serde(default)] pub networking: Networking, /// Slice de cgroup v2. Default `arje.slice/