diff --git a/Cargo.lock b/Cargo.lock index 4851dde..91d02b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,35 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "ente-hostnamed-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "libc", + "nix", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + +[[package]] +name = "ente-journald-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "libc", + "nix", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "ente-kernel" version = "0.0.1" @@ -390,6 +419,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "ente-localed-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + [[package]] name = "ente-logind-compat" version = "0.0.1" @@ -426,6 +468,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "ente-timedated-compat" +version = "0.0.1" +dependencies = [ + "anyhow", + "ente-bus", + "ente-card", + "tokio", + "tracing", + "tracing-subscriber", + "zbus", +] + [[package]] name = "ente-wasm" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 035d928..3f02310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ members = [ "crates/ente-zero", "crates/ente-echo", "crates/ente-logind-compat", + "crates/ente-hostnamed-compat", + "crates/ente-timedated-compat", + "crates/ente-localed-compat", + "crates/ente-journald-compat", ] [workspace.package] diff --git a/crates/ente-hostnamed-compat/Cargo.toml b/crates/ente-hostnamed-compat/Cargo.toml new file mode 100644 index 0000000..04e3568 --- /dev/null +++ b/crates/ente-hostnamed-compat/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "ente-hostnamed-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-hostnamed-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 } +zbus = { version = "4", default-features = false, features = ["tokio"] } diff --git a/crates/ente-hostnamed-compat/src/main.rs b/crates/ente-hostnamed-compat/src/main.rs new file mode 100644 index 0000000..4f78e83 --- /dev/null +++ b/crates/ente-hostnamed-compat/src/main.rs @@ -0,0 +1,259 @@ +//! ente-hostnamed-compat: shim de `org.freedesktop.hostname1`. +//! +//! GNOME control-center y otros componentes consultan este servicio al boot +//! para mostrar nombre de host, OS, kernel. Sin esto los settings panels +//! se rompen aunque el sistema funcione. +//! +//! Read-only properties: leemos /etc/hostname, /etc/os-release, uname(). +//! Set* methods: log + forward al bus interno (no aplicamos cambios reales +//! en el stub — un siguiente paso es persistir a /etc/* y rehash). + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::sync::Mutex; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface, Connection}; + +const BUS_NAME: &str = "org.freedesktop.hostname1"; +const OBJ_PATH: &str = "/org/freedesktop/hostname1"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-hostnamed-compat: arrancando"); + announce_to_fractal().await; + + let manager = HostnameManager::default(); + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { + warn!(?e, "build conn falló — modo idle"); + wait_for_term().await + } + }, + Err(e) => { + warn!(?e, "builder D-Bus falló — modo idle"); + wait_for_term().await + } + } +} + +#[derive(Default)] +struct HostnameManager { + /// Cache para SetHostname. En el stub no persistimos a /etc. + transient_hostname: Mutex>, +} + +#[interface(name = "org.freedesktop.hostname1")] +impl HostnameManager { + // ----- Properties read-only ----- + + #[zbus(property)] + async fn hostname(&self) -> String { + if let Some(h) = self.transient_hostname.lock().unwrap().clone() { + return h; + } + gethostname_libc().unwrap_or_else(|| "localhost".into()) + } + + #[zbus(property)] + async fn static_hostname(&self) -> String { + std::fs::read_to_string("/etc/hostname") + .map(|s| s.trim().to_string()) + .unwrap_or_default() + } + + #[zbus(property)] + async fn pretty_hostname(&self) -> String { + read_machine_info_field("PRETTY_HOSTNAME").unwrap_or_default() + } + + #[zbus(property)] + async fn icon_name(&self) -> String { + read_machine_info_field("ICON_NAME").unwrap_or_default() + } + + #[zbus(property)] + async fn chassis(&self) -> String { + read_machine_info_field("CHASSIS").unwrap_or_else(|| "desktop".into()) + } + + #[zbus(property)] + async fn deployment(&self) -> String { + read_machine_info_field("DEPLOYMENT").unwrap_or_default() + } + + #[zbus(property)] + async fn location(&self) -> String { + read_machine_info_field("LOCATION").unwrap_or_default() + } + + #[zbus(property)] + async fn kernel_name(&self) -> String { + nix::sys::utsname::uname() + .ok() + .and_then(|u| u.sysname().to_str().map(String::from)) + .unwrap_or_else(|| "Linux".into()) + } + + #[zbus(property)] + async fn kernel_release(&self) -> String { + nix::sys::utsname::uname() + .ok() + .and_then(|u| u.release().to_str().map(String::from)) + .unwrap_or_default() + } + + #[zbus(property)] + async fn kernel_version(&self) -> String { + nix::sys::utsname::uname() + .ok() + .and_then(|u| u.version().to_str().map(String::from)) + .unwrap_or_default() + } + + #[zbus(property)] + async fn operating_system_pretty_name(&self) -> String { + read_os_release_field("PRETTY_NAME").unwrap_or_else(|| "Linux".into()) + } + + #[zbus(property)] + async fn operating_system_cpename(&self) -> String { + read_os_release_field("CPE_NAME").unwrap_or_default() + } + + #[zbus(property)] + async fn home_url(&self) -> String { + read_os_release_field("HOME_URL").unwrap_or_default() + } + + #[zbus(property)] + async fn hardware_vendor(&self) -> String { + read_dmi("/sys/class/dmi/id/sys_vendor") + } + + #[zbus(property)] + async fn hardware_model(&self) -> String { + read_dmi("/sys/class/dmi/id/product_name") + } + + #[zbus(property)] + async fn firmware_version(&self) -> String { + read_dmi("/sys/class/dmi/id/bios_version") + } + + // ----- Setters: forward al bus interno y guardan en cache ----- + + async fn set_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> { + info!(%name, "SetHostname → bus interno (stub)"); + *self.transient_hostname.lock().unwrap() = Some(name); + Ok(()) + } + + async fn set_static_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> { + info!(%name, "SetStaticHostname (stub: no persistimos a /etc)"); + Ok(()) + } + + async fn set_pretty_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> { + info!(%name, "SetPrettyHostname (stub)"); + Ok(()) + } + + async fn set_icon_name(&self, name: String, _interactive: bool) -> fdo::Result<()> { + info!(%name, "SetIconName (stub)"); + Ok(()) + } + + async fn set_chassis(&self, chassis: String, _interactive: bool) -> fdo::Result<()> { + info!(%chassis, "SetChassis (stub)"); + Ok(()) + } + + async fn set_deployment(&self, deployment: String, _interactive: bool) -> fdo::Result<()> { + info!(%deployment, "SetDeployment (stub)"); + Ok(()) + } + + async fn set_location(&self, location: String, _interactive: bool) -> fdo::Result<()> { + info!(%location, "SetLocation (stub)"); + Ok(()) + } +} + +// ---------------- helpers ---------------- + +fn gethostname_libc() -> Option { + let mut buf = [0u8; 256]; + let r = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) }; + if r != 0 { return None; } + let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + std::str::from_utf8(&buf[..len]).ok().map(String::from) +} + +fn read_os_release_field(field: &str) -> Option { + parse_kv_file("/etc/os-release", field) +} + +fn read_machine_info_field(field: &str) -> Option { + parse_kv_file("/etc/machine-info", field) +} + +fn parse_kv_file(path: &str, field: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + for line in content.lines() { + if let Some((k, v)) = line.split_once('=') { + if k.trim() == field { + return Some(v.trim().trim_matches('"').to_string()); + } + } + } + None +} + +fn read_dmi(path: &str) -> String { + std::fs::read_to_string(path) + .map(|s| s.trim().to_string()) + .unwrap_or_default() +} + +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([0xa0; 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_hostnamed_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-journald-compat/Cargo.toml b/crates/ente-journald-compat/Cargo.toml new file mode 100644 index 0000000..4b74f11 --- /dev/null +++ b/crates/ente-journald-compat/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ente-journald-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-journald-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-journald-compat/src/main.rs b/crates/ente-journald-compat/src/main.rs new file mode 100644 index 0000000..3da7297 --- /dev/null +++ b/crates/ente-journald-compat/src/main.rs @@ -0,0 +1,166 @@ +//! ente-journald-compat: stub que absorbe escrituras al journal socket. +//! +//! Listen en `/run/systemd/journal/socket` (datagram) — todo lo que llega +//! se decodifica best-effort y se emite como tracing event. +//! +//! Sin esto, apps que usan `sd_journal_send` o syslog fallan al escribir. +//! Para una implementación real: persistir a CAS por timestamp+sha, +//! exponer query API, indexar por unidad/usuario. + +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 JOURNAL_SOCKET: &str = "/run/systemd/journal/socket"; +const DEV_LOG: &str = "/dev/log"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-journald-compat: arrancando"); + announce_to_fractal().await; + + // Intentamos vincular ambos sockets. Cada uno puede fallar + // independientemente; si alguno funciona, seguimos. + let mut bound = 0usize; + if let Some(stream) = bind_dgram(JOURNAL_SOCKET) { + bound += 1; + spawn_listener(stream, "journal"); + } else { + warn!(path = JOURNAL_SOCKET, "no se pudo bind — necesita CAP_NET_BIND_SERVICE o /run writable"); + } + if let Some(stream) = bind_dgram(DEV_LOG) { + bound += 1; + spawn_listener(stream, "syslog"); + } else { + warn!(path = DEV_LOG, "no se pudo bind /dev/log"); + } + + if bound == 0 { + warn!("ningún socket bound — modo idle"); + } else { + info!(sockets_bound = bound, "journald-compat listening"); + } + + 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 = match socket( + AddressFamily::Unix, + SockType::Datagram, + SockFlag::SOCK_NONBLOCK | SockFlag::SOCK_CLOEXEC, + None, + ) { + Ok(f) => f, + Err(e) => { warn!(?e, "socket() falló"); return None; } + }; + let addr = match UnixAddr::new(path) { + Ok(a) => a, + Err(e) => { warn!(?e, "UnixAddr falló"); return None; } + }; + if let Err(e) = bind(fd.as_raw_fd(), &addr) { + warn!(?e, %path, "bind falló"); + return None; + } + 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, source: &'static str) { + tokio::spawn(async move { + let mut buf = vec![0u8; 64 * 1024]; + loop { + let mut guard = match async_fd.readable().await { + Ok(g) => g, + Err(e) => { warn!(?e, source, "readable failed"); 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_message(&buf[..n as usize], source); + } + guard.clear_ready(); + } + }); +} + +/// Decodifica best-effort. Formato journald nativo: lines de "KEY=value" +/// (binario para values con newlines, pero raro). Formato syslog: texto +/// con prefijo "tag: message". +fn handle_message(buf: &[u8], source: &'static str) { + if let Ok(s) = std::str::from_utf8(buf) { + // Heurística: si tiene '=' en alguna línea, asumir journald. + if s.contains('=') && s.lines().any(|l| l.contains('=')) { + let mut message = None; + let mut priority = None; + let mut unit = None; + for line in s.lines() { + if let Some((k, v)) = line.split_once('=') { + match k { + "MESSAGE" => message = Some(v.to_string()), + "PRIORITY" => priority = Some(v.to_string()), + "_SYSTEMD_UNIT" | "UNIT" => unit = Some(v.to_string()), + _ => {} + } + } + } + if let Some(msg) = message { + info!(target: "journal", source, ?priority, ?unit, "{msg}"); + } else { + debug!(source, len = buf.len(), "journal native sin MESSAGE"); + } + } else { + // Syslog + info!(target: "syslog", source, "{}", s.trim_end()); + } + } else { + debug!(source, len = buf.len(), "journal binario (no UTF-8)"); + } +} + +async fn announce_to_fractal() { + if let Ok(mut client) = BusClient::from_env().await { + let req = BusRequest::Announce { + capabilities: vec![Capability::Journal], + }; + 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_journald_compat=info,journal=info,syslog=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-localed-compat/Cargo.toml b/crates/ente-localed-compat/Cargo.toml new file mode 100644 index 0000000..04a5c5b --- /dev/null +++ b/crates/ente-localed-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-localed-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-localed-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-localed-compat/src/main.rs b/crates/ente-localed-compat/src/main.rs new file mode 100644 index 0000000..24e3241 --- /dev/null +++ b/crates/ente-localed-compat/src/main.rs @@ -0,0 +1,188 @@ +//! ente-localed-compat: shim de `org.freedesktop.locale1`. +//! +//! GNOME settings panel "Region & Language" llama aquí. Properties leen +//! /etc/locale.conf y /etc/vconsole.conf; setters log + forward. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::sync::Mutex; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface}; + +const BUS_NAME: &str = "org.freedesktop.locale1"; +const OBJ_PATH: &str = "/org/freedesktop/locale1"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-localed-compat: arrancando"); + announce_to_fractal().await; + + let manager = LocaleManager::default(); + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { + warn!(?e, "build conn falló — modo idle"); + wait_for_term().await + } + }, + Err(e) => { + warn!(?e, "builder D-Bus falló — modo idle"); + wait_for_term().await + } + } +} + +#[derive(Default)] +struct LocaleManager { + transient_locale: Mutex>>, +} + +#[interface(name = "org.freedesktop.locale1")] +impl LocaleManager { + /// Locale actual como array de "KEY=value" (LANG=en_US.UTF-8, LC_TIME=...). + /// Default: leer /etc/locale.conf. + #[zbus(property)] + async fn locale(&self) -> Vec { + if let Some(v) = self.transient_locale.lock().unwrap().clone() { + return v; + } + match std::fs::read_to_string("/etc/locale.conf") { + Ok(c) => c.lines() + .filter(|l| !l.trim().is_empty() && !l.starts_with('#')) + .map(|s| s.trim().to_string()) + .collect(), + Err(_) => vec!["LANG=C.UTF-8".into()], + } + } + + #[zbus(property)] + async fn x11layout(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbLayout").unwrap_or_default() + } + + #[zbus(property)] + async fn x11model(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbModel").unwrap_or_default() + } + + #[zbus(property)] + async fn x11variant(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbVariant").unwrap_or_default() + } + + #[zbus(property)] + async fn x11options(&self) -> String { + read_kv("/etc/X11/xorg.conf.d/00-keyboard.conf", "XkbOptions").unwrap_or_default() + } + + #[zbus(property)] + async fn vconsole_keymap(&self) -> String { + read_vconsole("KEYMAP").unwrap_or_default() + } + + #[zbus(property)] + async fn vconsole_keymap_toggle(&self) -> String { + read_vconsole("KEYMAP_TOGGLE").unwrap_or_default() + } + + async fn set_locale(&self, locale: Vec, _interactive: bool) -> fdo::Result<()> { + info!(?locale, "SetLocale (stub: no persistimos a /etc/locale.conf)"); + *self.transient_locale.lock().unwrap() = Some(locale); + Ok(()) + } + + async fn set_vconsole_keymap( + &self, + keymap: String, + keymap_toggle: String, + _convert: bool, + _interactive: bool, + ) -> fdo::Result<()> { + info!(%keymap, %keymap_toggle, "SetVConsoleKeymap (stub)"); + Ok(()) + } + + async fn set_x11_keyboard( + &self, + layout: String, + model: String, + variant: String, + options: String, + _convert: bool, + _interactive: bool, + ) -> fdo::Result<()> { + info!(%layout, %model, %variant, %options, "SetX11Keyboard (stub)"); + Ok(()) + } +} + +fn read_kv(path: &str, key: &str) -> Option { + let content = std::fs::read_to_string(path).ok()?; + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with(&format!("Option \"{key}\"")) || trimmed.starts_with(key) { + // Best-effort parse: tomar lo que está entre comillas. + if let Some(start) = trimmed.find('"') { + let rest = &trimmed[start + 1..]; + if let Some(end) = rest.find('"') { + return Some(rest[..end].to_string()); + } + } + } + } + None +} + +fn read_vconsole(key: &str) -> Option { + let content = std::fs::read_to_string("/etc/vconsole.conf").ok()?; + for line in content.lines() { + if let Some((k, v)) = line.split_once('=') { + if k.trim() == key { + return Some(v.trim().trim_matches('"').to_string()); + } + } + } + None +} + +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([0xa2; 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_localed_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-timedated-compat/Cargo.toml b/crates/ente-timedated-compat/Cargo.toml new file mode 100644 index 0000000..4a38e51 --- /dev/null +++ b/crates/ente-timedated-compat/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ente-timedated-compat" +version = "0.0.1" +edition.workspace = true +license.workspace = true +publish.workspace = true + +[[bin]] +name = "ente-timedated-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-timedated-compat/src/main.rs b/crates/ente-timedated-compat/src/main.rs new file mode 100644 index 0000000..1ad2b0b --- /dev/null +++ b/crates/ente-timedated-compat/src/main.rs @@ -0,0 +1,168 @@ +//! ente-timedated-compat: shim de `org.freedesktop.timedate1`. +//! +//! GNOME settings panel "Date & Time" llama aquí. Properties read-only se +//! mapean a syscalls/lecturas del sistema; setters log + forward. + +use ente_bus::{BusClient, BusRequest, BusResponse}; +use ente_card::Capability; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::signal::unix::{signal, SignalKind}; +use tracing::{info, warn}; +use tracing_subscriber::EnvFilter; +use zbus::{fdo, interface}; + +const BUS_NAME: &str = "org.freedesktop.timedate1"; +const OBJ_PATH: &str = "/org/freedesktop/timedate1"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + init_tracing(); + info!("ente-timedated-compat: arrancando"); + announce_to_fractal().await; + + let manager = TimedateManager::default(); + let conn_result = zbus::connection::Builder::system() + .and_then(|b| b.name(BUS_NAME)) + .and_then(|b| b.serve_at(OBJ_PATH, manager)); + match conn_result { + Ok(builder) => match builder.build().await { + Ok(_conn) => { + info!(name = BUS_NAME, "name acquired, sirviendo"); + wait_for_term().await + } + Err(e) => { + warn!(?e, "build conn falló — modo idle"); + wait_for_term().await + } + }, + Err(e) => { + warn!(?e, "builder D-Bus falló — modo idle"); + wait_for_term().await + } + } +} + +#[derive(Default)] +struct TimedateManager; + +#[interface(name = "org.freedesktop.timedate1")] +impl TimedateManager { + // ----- Properties ----- + + /// Timezone configurada. Por defecto leemos el target de /etc/localtime + /// (un symlink a /usr/share/zoneinfo/). + #[zbus(property)] + async fn timezone(&self) -> String { + std::fs::read_link("/etc/localtime") + .ok() + .and_then(|p| { + let s = p.to_string_lossy().into_owned(); + s.strip_prefix("/usr/share/zoneinfo/").map(String::from) + .or_else(|| s.split("/zoneinfo/").nth(1).map(String::from)) + }) + .unwrap_or_else(|| "UTC".into()) + } + + /// True si el RTC del hardware está en local time. Convención moderna + /// es UTC (false). Reportamos false como default. + #[zbus(property)] + async fn local_rtc(&self) -> bool { false } + + /// Si NTP es soportado. Reportamos true (asumimos systemd-timesyncd + /// o chrony están disponibles en el host). + #[zbus(property)] + async fn can_ntp(&self) -> bool { true } + + /// Si NTP está activo. Sin daemon real bajo nuestro control no podemos + /// consultarlo con precisión — false como default seguro. + #[zbus(property)] + async fn ntp(&self) -> bool { false } + + #[zbus(property)] + async fn ntpsynchronized(&self) -> bool { false } + + /// Timestamp actual en microsegundos desde epoch. + #[zbus(property)] + async fn time_usec(&self) -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH) + .map(|d| d.as_micros() as u64) + .unwrap_or(0) + } + + #[zbus(property)] + async fn rtctime_usec(&self) -> u64 { + // El RTC real requiere ioctl a /dev/rtc — usamos system clock como aprox. + SystemTime::now().duration_since(UNIX_EPOCH) + .map(|d| d.as_micros() as u64) + .unwrap_or(0) + } + + // ----- Setters ----- + + async fn set_time(&self, usec_utc: i64, _relative: bool, _interactive: bool) -> fdo::Result<()> { + info!(usec_utc, "SetTime (stub: requiere CAP_SYS_TIME para aplicar)"); + Ok(()) + } + + async fn set_timezone(&self, timezone: String, _interactive: bool) -> fdo::Result<()> { + info!(%timezone, "SetTimezone (stub: no actualizamos /etc/localtime)"); + Ok(()) + } + + async fn set_local_rtc(&self, local_rtc: bool, _fix_system: bool, _interactive: bool) -> fdo::Result<()> { + info!(local_rtc, "SetLocalRTC (stub)"); + Ok(()) + } + + async fn set_ntp(&self, ntp: bool, _interactive: bool) -> fdo::Result<()> { + info!(ntp, "SetNTP (stub: no controlamos timesyncd)"); + Ok(()) + } + + async fn list_timezones(&self) -> fdo::Result> { + // Listar /usr/share/zoneinfo recursivamente. Hacemos un best-effort. + let mut out = Vec::new(); + if let Ok(rd) = std::fs::read_dir("/usr/share/zoneinfo") { + for entry in rd.flatten() { + if let Ok(name) = entry.file_name().into_string() { + if !name.starts_with(|c: char| c.is_lowercase()) && name != "posix" && name != "right" { + out.push(name); + } + } + } + } + Ok(out) + } +} + +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([0xa1; 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_timedated_compat=info")); + tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init(); +} diff --git a/crates/ente-zero/src/seed.rs b/crates/ente-zero/src/seed.rs index bf8acb4..9dfada5 100644 --- a/crates/ente-zero/src/seed.rs +++ b/crates/ente-zero/src/seed.rs @@ -146,6 +146,23 @@ fn synthesize_dev_seed() -> EntityCard { genesis.push(card); } + // Constelación de shims D-Bus que reemplazan systemd: cada uno provee + // un nombre `org.freedesktop.X1` que GNOME/KDE consultan al boot. + for (label, bin) in &[ + ("compat-hostnamed", "target/debug/ente-hostnamed-compat"), + ("compat-timedated", "target/debug/ente-timedated-compat"), + ("compat-localed", "target/debug/ente-localed-compat"), + ("compat-journald", "target/debug/ente-journald-compat"), + ] { + if let Some(card) = optional_native_card( + label, bin, + std::collections::BTreeSet::new(), + restart_supervision(), + ) { + genesis.push(card); + } + } + EntityCard { schema_version: CARD_SCHEMA_VERSION, id: Ulid::new(),