diff --git a/Cargo.lock b/Cargo.lock index 6970d6f..ef90170 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,6 +317,15 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" +[[package]] +name = "ente-binfmt-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ente-brain" version = "0.0.1" @@ -459,6 +468,20 @@ dependencies = [ "zbus", ] +[[package]] +name = "ente-notify-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "libc", + "nix", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ente-policy-provider" version = "0.0.1" @@ -523,6 +546,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "ente-systemd1-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + [[package]] name = "ente-timedated-compat" version = "0.0.1" @@ -536,6 +572,21 @@ dependencies = [ "zbus", ] +[[package]] +name = "ente-timer-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", + "ulid", +] + [[package]] name = "ente-tmpfiles-compat" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 3b3540a..c5c4bf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,10 @@ members = [ "crates/ente-machined-compat", "crates/ente-policy-provider", "crates/ente-tmpfiles-compat", + "crates/ente-systemd1-compat", + "crates/ente-notify-compat", + "crates/ente-binfmt-compat", + "crates/ente-timer-compat", ] [workspace.package] diff --git a/crates/ente-binfmt-compat/Cargo.toml b/crates/ente-binfmt-compat/Cargo.toml new file mode 100644 index 0000000..f972677 --- /dev/null +++ b/crates/ente-binfmt-compat/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ente-binfmt-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-binfmt-compat" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/ente-binfmt-compat/src/main.rs b/crates/ente-binfmt-compat/src/main.rs new file mode 100644 index 0000000..9789d5c --- /dev/null +++ b/crates/ente-binfmt-compat/src/main.rs @@ -0,0 +1,109 @@ +//! ente-binfmt-compat: registra handlers de binfmt_misc al boot. +//! +//! systemd-binfmt lee `/usr/lib/binfmt.d/*.conf` y `/etc/binfmt.d/*.conf` y +//! escribe cada línea al kernel via `/proc/sys/fs/binfmt_misc/register`. +//! Esto habilita ejecución transparente de binarios no-ELF (qemu-user, +//! wine, etc). +//! +//! Formato de cada línea: +//! ::::::: +//! +//! Líneas que empiezan con `#` o vacías se ignoran. + +use std::fs; +use std::io::Write; +use std::path::Path; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; + +const REGISTER_PATH: &str = "/proc/sys/fs/binfmt_misc/register"; +const SEARCH_DIRS: &[&str] = &[ + "/usr/lib/binfmt.d", + "/etc/binfmt.d", + "/run/binfmt.d", +]; + +fn main() { + init_tracing(); + info!("ente-binfmt-compat: registrando handlers binfmt_misc"); + + if !Path::new(REGISTER_PATH).exists() { + warn!(path = REGISTER_PATH, "binfmt_misc no montado — skip"); + std::process::exit(0); + } + + let mut registered = 0; + let mut errors = 0; + let mut skipped = 0; + + for dir in SEARCH_DIRS { + if !Path::new(dir).exists() { continue; } + let mut entries: Vec<_> = match fs::read_dir(dir) { + Ok(rd) => rd.filter_map(|e| e.ok()).collect(), + Err(_) => continue, + }; + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + let path = entry.path(); + if path.extension().map(|e| e != "conf").unwrap_or(true) { continue; } + let content = match fs::read_to_string(&path) { + Ok(c) => c, + Err(e) => { warn!(?e, path = %path.display(), "read"); continue; } + }; + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { continue; } + match register(line) { + Ok(name) => { + info!(file = %path.display(), %name, "binfmt registrado"); + registered += 1; + } + Err(e) => { + if e.is_already_exists() { + skipped += 1; + } else { + warn!(?e, file = %path.display(), "registro falló"); + errors += 1; + } + } + } + } + } + } + info!(registered, skipped, errors, "binfmt aplicado"); + if errors > 0 { std::process::exit(1); } +} + +#[derive(Debug)] +struct RegError(std::io::Error); +impl std::fmt::Display for RegError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } +} +impl RegError { + fn is_already_exists(&self) -> bool { + // EEXIST = 17 en Linux. + self.0.raw_os_error() == Some(17) + } +} + +/// Escribe la línea al register file. Devuelve el `name` extraído del +/// primer campo (entre `:` separators) si tuvo éxito. +fn register(line: &str) -> Result { + // Sintaxis: ::::::: + // Field 0 (después del ':' inicial) es el name. + let name = line.split(':').nth(1) + .map(|s| s.to_string()) + .unwrap_or_else(|| "?".into()); + let mut f = fs::OpenOptions::new() + .write(true) + .open(REGISTER_PATH) + .map_err(RegError)?; + f.write_all(line.as_bytes()).map_err(RegError)?; + Ok(name) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_binfmt_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-notify-compat/Cargo.toml b/crates/ente-notify-compat/Cargo.toml new file mode 100644 index 0000000..fdba47a --- /dev/null +++ b/crates/ente-notify-compat/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ente-notify-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-notify-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +nix = { workspace = true } +libc = { workspace = true } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/ente-notify-compat/src/main.rs b/crates/ente-notify-compat/src/main.rs new file mode 100644 index 0000000..afcf275 --- /dev/null +++ b/crates/ente-notify-compat/src/main.rs @@ -0,0 +1,160 @@ +//! ente-notify-compat: NOTIFY_SOCKET listener para apps `Type=notify`. +//! +//! systemd convention: el servicio escribe `KEY=value\n` lines a un socket +//! datagram cuya path está en `$NOTIFY_SOCKET`. Keys típicos: +//! - READY=1 (servicio listo para recibir requests) +//! - STATUS=text (descripción del estado) +//! - WATCHDOG=1 (heartbeat) +//! - STOPPING=1 (cierre ordenado) +//! - MAINPID= (cambio de PID principal) +//! +//! Path canonical: /run/systemd/notify. Bindeable sólo con CAP_NET_BIND_SERVICE +//! o si /run es writable. +//! +//! Para que las apps lo usen, ente-soma debe inyectar `NOTIFY_SOCKET=` +//! en el envp de cada Ente encarnado. Eso ya lo hace via build_env() — +//! aquí sólo necesitamos que el path sea coherente. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::os::fd::{AsRawFd, OwnedFd}; +use std::path::Path; +use tokio::io::unix::AsyncFd; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{debug, info, warn}; +use tracing_subscriber::EnvFilter; + +const NOTIFY_SOCKET_PATH: &str = "/run/systemd/notify"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!(path = NOTIFY_SOCKET_PATH, "ente-notify-compat: arrancando"); + announce_to_fractal().await; + + let stream = match bind_dgram(NOTIFY_SOCKET_PATH) { + Some(s) => s, + None => { + warn!("no se pudo bind — modo idle (apps Type=notify caerán a no-op)"); + return wait_for_term().await; + } + }; + info!("NOTIFY_SOCKET listening"); + spawn_listener(stream); + wait_for_term().await +} + +fn bind_dgram(path: &str) -> Option> { + use nix::sys::socket::{bind, socket, AddressFamily, SockFlag, SockType, UnixAddr}; + let _ = std::fs::remove_file(path); + if let Some(parent) = Path::new(path).parent() { + let _ = std::fs::create_dir_all(parent); + } + let fd = socket( + AddressFamily::Unix, + SockType::Datagram, + SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC, + None, + ).ok()?; + let addr = UnixAddr::new(path).ok()?; + if let Err(e) = bind(fd.as_raw_fd(), &addr) { + warn!(?e, %path, "bind"); + return None; + } + // Permisos abiertos: cualquier proceso debería poder escribir notificaciones. + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o666)); + AsyncFd::new(OwnedFdWrap(fd)).ok() +} + +struct OwnedFdWrap(OwnedFd); +impl AsRawFd for OwnedFdWrap { + fn as_raw_fd(&self) -> std::os::fd::RawFd { self.0.as_raw_fd() } +} + +fn spawn_listener(async_fd: AsyncFd) { + tokio::spawn(async move { + let mut buf = vec![0u8; 16 * 1024]; + loop { + let mut guard = match async_fd.readable().await { + Ok(g) => g, + Err(e) => { warn!(?e, "readable"); return; } + }; + let raw_fd = guard.get_inner().as_raw_fd(); + loop { + let n = unsafe { libc::recv(raw_fd, buf.as_mut_ptr() as *mut _, buf.len(), 0) }; + if n <= 0 { break; } + handle_notification(&buf[..n as usize]); + } + guard.clear_ready(); + } + }); +} + +fn handle_notification(buf: &[u8]) { + let s = match std::str::from_utf8(buf) { + Ok(s) => s, + Err(_) => { debug!(len = buf.len(), "notify binario, skip"); return; } + }; + let mut ready = false; + let mut status = None; + let mut mainpid = None; + let mut watchdog = false; + let mut stopping = false; + let mut other_keys = Vec::new(); + for line in s.lines() { + if let Some((k, v)) = line.split_once('=') { + match k { + "READY" if v == "1" => ready = true, + "STATUS" => status = Some(v.to_string()), + "MAINPID" => mainpid = v.parse::().ok(), + "WATCHDOG" if v == "1" => watchdog = true, + "STOPPING" if v == "1" => stopping = true, + _ => other_keys.push(format!("{k}={v}")), + } + } + } + if ready { + info!(?status, ?mainpid, "sd_notify READY"); + } else if stopping { + info!(?status, "sd_notify STOPPING"); + } else if watchdog { + debug!("sd_notify WATCHDOG"); + } else if let Some(s) = status { + info!(%s, "sd_notify STATUS"); + } else if !other_keys.is_empty() { + debug!(keys = ?other_keys, "sd_notify (other)"); + } +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Endpoint { + interface: ente_card::InterfaceId([0xa7; 16]), + version: 1, + }], + }; + match client.call(req).await { + Ok(BusResponse::Ok) => info!("Announce → bus interno OK"), + Ok(other) => warn!(?other, "Announce respuesta inesperada"), + Err(e) => warn!(?e, "Announce falló"), + } + } +} + +async fn wait_for_term() -> anyhow::Result<()> { + let mut term = signal(SignalKind::terminate())?; + let mut int_ = signal(SignalKind::interrupt())?; + tokio::select! { + _ = term.recv() => info!("SIGTERM"), + _ = int_.recv() => info!("SIGINT"), + } + Ok(()) +} + +fn init_tracing() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("ente_notify_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-soma/src/lib.rs b/crates/ente-soma/src/lib.rs index e3e8c44..e64ca01 100644 --- a/crates/ente-soma/src/lib.rs +++ b/crates/ente-soma/src/lib.rs @@ -57,6 +57,11 @@ fn build_env(card: &EntityCard, base_envp: &[(String, String)]) -> Vec<(String, } env.retain(|(k, _)| k != ente_bus::ENV_ENTE_ID); env.push((ente_bus::ENV_ENTE_ID.into(), card.id.to_string())); + // Apps `Type=notify` (sd_notify) leen NOTIFY_SOCKET. Apuntamos al path + // canónico de systemd; si ente-notify-compat no está corriendo, apps + // sólo verán que sd_notify falla y siguen sin "ready" signal — no es fatal. + env.retain(|(k, _)| k != "NOTIFY_SOCKET"); + env.push(("NOTIFY_SOCKET".into(), "/run/systemd/notify".into())); env } diff --git a/crates/ente-systemd1-compat/Cargo.toml b/crates/ente-systemd1-compat/Cargo.toml new file mode 100644 index 0000000..425521e --- /dev/null +++ b/crates/ente-systemd1-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-systemd1-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-systemd1-compat" +path = "src/main.rs" + +[dependencies] +ente-card = { path = "../ente-card" } +ente-bus = { path = "../ente-bus" } +anyhow = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/ente-systemd1-compat/src/main.rs b/crates/ente-systemd1-compat/src/main.rs new file mode 100644 index 0000000..81ae1ba --- /dev/null +++ b/crates/ente-systemd1-compat/src/main.rs @@ -0,0 +1,280 @@ +//! ente-systemd1-compat: shim de `org.freedesktop.systemd1.Manager`. +//! +//! Centro de control que `systemctl` consulta. Sin esto, `systemctl list-units` +//! falla con `Failed to connect to bus` aunque el sistema funcione. +//! +//! Mapeo: cada Ente vivo del fractal aparece como una "unit" cuyo nombre es +//! `