Files
arje/crates/ente-notify-compat/src/main.rs
T
Sergio 227715a464 compat-systemd1, NOTIFY_SOCKET, binfmt y timer
- ente-systemd1-compat: org.freedesktop.systemd1.Manager. Cada Ente vivo
  del fractal aparece como `<label>.service`. ListUnits/GetUnit/
  GetUnitByPID consultan al bus interno (BusRequest::ListEntes). Start/
  Stop/Restart son stubs que loguen — Cards ya están en el grafo, no se
  inician/paran on-demand. Reload/ListUnitFiles/ListJobs vacíos.
  Properties: Version, Architecture, Features, Environment.

- ente-notify-compat: listener en /run/systemd/notify para sd_notify.
  Decodifica KEY=value lines (READY/STATUS/MAINPID/WATCHDOG/STOPPING)
  y log estructurado. Permisos 0666 para que cualquier proceso escriba.
- ente-soma inyecta NOTIFY_SOCKET=/run/systemd/notify en cada Ente
  encarnado (junto a ENTE_BUS_SOCK + ENTE_ID).

- ente-binfmt-compat: lee /usr/lib/binfmt.d, /etc/binfmt.d, /run/binfmt.d
  en orden. Cada línea no-comment se escribe a /proc/sys/fs/binfmt_misc/
  register. EEXIST silencioso (handler ya registrado por boot anterior).
  OneShot pattern.

- ente-timer-compat: scheduler cron 5-field UTC (min hour dom mon dow).
  Soporta `*`, `N`, `*/N` por field. Lee /etc/ente/timers.json. Tick
  alineado al próximo minuto exacto, evalúa todos los timers cada
  60s. Decompose epoch via Howard Hinnant Civil from days. fire() loguea
  por ahora — spawn real requiere SpawnRequest via bus.

3 shims añadidos: 0xa6 systemd1, 0xa7 notify, 0xa8 timer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:43:39 +00:00

161 lines
5.5 KiB
Rust

//! 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=<pid> (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=<path>`
//! 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<AsyncFd<OwnedFdWrap>> {
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<OwnedFdWrap>) {
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::<u32>().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();
}