gioser
⎇ d88a9c57914ac7870b43a7a4f616510161278237 crates/ente-hostnamed-compat/src/main.rs Rust · 340 líneas · 11.5 KB Historia Quién tocó qué crudo

Persistencia setters, compat-resolved, journald→CAS, compat-polkit d88a9c579 · Sergio ·

1//! ente-hostnamed-compat: shim de `org.freedesktop.hostname1`.
2//!
3//! GNOME control-center y otros componentes consultan este servicio al boot
4//! para mostrar nombre de host, OS, kernel. Sin esto los settings panels
5//! se rompen aunque el sistema funcione.
6//!
7//! Read-only properties: leemos /etc/hostname, /etc/os-release, uname().
8//! Set* methods: log + forward al bus interno (no aplicamos cambios reales
9//! en el stub — un siguiente paso es persistir a /etc/* y rehash).
10
11use ente_bus::{BusClient, BusRequest, BusResponse};
12use ente_card::Capability;
13use std::sync::Mutex;
14use tokio::signal::unix::{signal, SignalKind};
15use tracing::{info, warn};
16use tracing_subscriber::EnvFilter;
17use zbus::{fdo, interface, Connection};
18
19const BUS_NAME: &str = "org.freedesktop.hostname1";
20const OBJ_PATH: &str = "/org/freedesktop/hostname1";
21
22#[tokio::main(flavor = "current_thread")]
23async fn main() -> anyhow::Result<()> {
24    init_tracing();
25    info!("ente-hostnamed-compat: arrancando");
26    announce_to_fractal().await;
27
28    let manager = HostnameManager::default();
29    let conn_result = zbus::connection::Builder::system()
30        .and_then(|b| b.name(BUS_NAME))
31        .and_then(|b| b.serve_at(OBJ_PATH, manager));
32    match conn_result {
33        Ok(builder) => match builder.build().await {
34            Ok(_conn) => {
35                info!(name = BUS_NAME, "name acquired, sirviendo");
36                wait_for_term().await
37            }
38            Err(e) => {
39                warn!(?e, "build conn falló — modo idle");
40                wait_for_term().await
41            }
42        },
43        Err(e) => {
44            warn!(?e, "builder D-Bus falló — modo idle");
45            wait_for_term().await
46        }
47    }
48}
49
50#[derive(Default)]
51struct HostnameManager {
52    /// Cache para SetHostname. En el stub no persistimos a /etc.
53    transient_hostname: Mutex<Option<String>>,
54}
55
56#[interface(name = "org.freedesktop.hostname1")]
57impl HostnameManager {
58    // ----- Properties read-only -----
59
60    #[zbus(property)]
61    async fn hostname(&self) -> String {
62        if let Some(h) = self.transient_hostname.lock().unwrap().clone() {
63            return h;
64        }
65        gethostname_libc().unwrap_or_else(|| "localhost".into())
66    }
67
68    #[zbus(property)]
69    async fn static_hostname(&self) -> String {
70        std::fs::read_to_string("/etc/hostname")
71            .map(|s| s.trim().to_string())
72            .unwrap_or_default()
73    }
74
75    #[zbus(property)]
76    async fn pretty_hostname(&self) -> String {
77        read_machine_info_field("PRETTY_HOSTNAME").unwrap_or_default()
78    }
79
80    #[zbus(property)]
81    async fn icon_name(&self) -> String {
82        read_machine_info_field("ICON_NAME").unwrap_or_default()
83    }
84
85    #[zbus(property)]
86    async fn chassis(&self) -> String {
87        read_machine_info_field("CHASSIS").unwrap_or_else(|| "desktop".into())
88    }
89
90    #[zbus(property)]
91    async fn deployment(&self) -> String {
92        read_machine_info_field("DEPLOYMENT").unwrap_or_default()
93    }
94
95    #[zbus(property)]
96    async fn location(&self) -> String {
97        read_machine_info_field("LOCATION").unwrap_or_default()
98    }
99
100    #[zbus(property)]
101    async fn kernel_name(&self) -> String {
102        nix::sys::utsname::uname()
103            .ok()
104            .and_then(|u| u.sysname().to_str().map(String::from))
105            .unwrap_or_else(|| "Linux".into())
106    }
107
108    #[zbus(property)]
109    async fn kernel_release(&self) -> String {
110        nix::sys::utsname::uname()
111            .ok()
112            .and_then(|u| u.release().to_str().map(String::from))
113            .unwrap_or_default()
114    }
115
116    #[zbus(property)]
117    async fn kernel_version(&self) -> String {
118        nix::sys::utsname::uname()
119            .ok()
120            .and_then(|u| u.version().to_str().map(String::from))
121            .unwrap_or_default()
122    }
123
124    #[zbus(property)]
125    async fn operating_system_pretty_name(&self) -> String {
126        read_os_release_field("PRETTY_NAME").unwrap_or_else(|| "Linux".into())
127    }
128
129    #[zbus(property)]
130    async fn operating_system_cpename(&self) -> String {
131        read_os_release_field("CPE_NAME").unwrap_or_default()
132    }
133
134    #[zbus(property)]
135    async fn home_url(&self) -> String {
136        read_os_release_field("HOME_URL").unwrap_or_default()
137    }
138
139    #[zbus(property)]
140    async fn hardware_vendor(&self) -> String {
141        read_dmi("/sys/class/dmi/id/sys_vendor")
142    }
143
144    #[zbus(property)]
145    async fn hardware_model(&self) -> String {
146        read_dmi("/sys/class/dmi/id/product_name")
147    }
148
149    #[zbus(property)]
150    async fn firmware_version(&self) -> String {
151        read_dmi("/sys/class/dmi/id/bios_version")
152    }
153
154    // ----- Setters: forward al bus interno y guardan en cache -----
155
156    async fn set_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
157        if !is_valid_hostname(&name) {
158            return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}")));
159        }
160        // sethostname(2) cambia sólo el running kernel value.
161        let cstr = std::ffi::CString::new(name.clone())
162            .map_err(|e| fdo::Error::Failed(format!("CString: {e}")))?;
163        let r = unsafe { libc::sethostname(cstr.as_ptr(), name.len()) };
164        if r != 0 {
165            warn!(error = %std::io::Error::last_os_error(), %name, "sethostname syscall falló (¿CAP_SYS_ADMIN?)");
166            // No es fatal — guardamos transient para que el property lea el valor nuevo.
167        }
168        *self.transient_hostname.lock().unwrap() = Some(name.clone());
169        info!(%name, "SetHostname aplicado");
170        Ok(())
171    }
172
173    async fn set_static_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
174        if !is_valid_hostname(&name) {
175            return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}")));
176        }
177        atomic_write("/etc/hostname", format!("{name}\n").as_bytes())
178            .map_err(|e| fdo::Error::Failed(format!("write /etc/hostname: {e}")))?;
179        info!(%name, "SetStaticHostname → /etc/hostname");
180        Ok(())
181    }
182
183    async fn set_pretty_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
184        update_machine_info("PRETTY_HOSTNAME", &name)
185            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
186        info!(%name, "SetPrettyHostname → /etc/machine-info");
187        Ok(())
188    }
189
190    async fn set_icon_name(&self, name: String, _interactive: bool) -> fdo::Result<()> {
191        update_machine_info("ICON_NAME", &name)
192            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
193        info!(%name, "SetIconName → /etc/machine-info");
194        Ok(())
195    }
196
197    async fn set_chassis(&self, chassis: String, _interactive: bool) -> fdo::Result<()> {
198        if !matches!(chassis.as_str(), "desktop"|"laptop"|"server"|"tablet"|"handset"|"watch"|"embedded"|"vm"|"container") {
199            return Err(fdo::Error::InvalidArgs(format!("chassis inválido: {chassis}")));
200        }
201        update_machine_info("CHASSIS", &chassis)
202            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
203        info!(%chassis, "SetChassis → /etc/machine-info");
204        Ok(())
205    }
206
207    async fn set_deployment(&self, deployment: String, _interactive: bool) -> fdo::Result<()> {
208        update_machine_info("DEPLOYMENT", &deployment)
209            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
210        info!(%deployment, "SetDeployment → /etc/machine-info");
211        Ok(())
212    }
213
214    async fn set_location(&self, location: String, _interactive: bool) -> fdo::Result<()> {
215        update_machine_info("LOCATION", &location)
216            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
217        info!(%location, "SetLocation → /etc/machine-info");
218        Ok(())
219    }
220}
221
222// ---------------- helpers ----------------
223
224fn gethostname_libc() -> Option<String> {
225    let mut buf = [0u8; 256];
226    let r = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) };
227    if r != 0 { return None; }
228    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
229    std::str::from_utf8(&buf[..len]).ok().map(String::from)
230}
231
232fn read_os_release_field(field: &str) -> Option<String> {
233    parse_kv_file("/etc/os-release", field)
234}
235
236fn read_machine_info_field(field: &str) -> Option<String> {
237    parse_kv_file("/etc/machine-info", field)
238}
239
240fn parse_kv_file(path: &str, field: &str) -> Option<String> {
241    let content = std::fs::read_to_string(path).ok()?;
242    for line in content.lines() {
243        if let Some((k, v)) = line.split_once('=') {
244            if k.trim() == field {
245                return Some(v.trim().trim_matches('"').to_string());
246            }
247        }
248    }
249    None
250}
251
252fn read_dmi(path: &str) -> String {
253    std::fs::read_to_string(path)
254        .map(|s| s.trim().to_string())
255        .unwrap_or_default()
256}
257
258/// RFC 1123 + extra: ASCII alfanumérico, dash, dot. Longitud 1..253.
259/// Rechaza vacíos, espacios, control chars.
260fn is_valid_hostname(s: &str) -> bool {
261    if s.is_empty() || s.len() > 253 { return false; }
262    s.chars().all(|c|
263        c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_'
264    )
265}
266
267/// Escritura atómica via tmp + rename. fsync del directorio para
268/// garantizar durabilidad post-crash. Permisos 0644.
269fn atomic_write(path: &str, content: &[u8]) -> std::io::Result<()> {
270    use std::io::Write;
271    use std::os::unix::fs::OpenOptionsExt;
272    let p = std::path::Path::new(path);
273    if let Some(parent) = p.parent() { let _ = std::fs::create_dir_all(parent); }
274    let tmp = p.with_extension("tmp");
275    {
276        let mut f = std::fs::OpenOptions::new()
277            .create(true).write(true).truncate(true)
278            .mode(0o644)
279            .open(&tmp)?;
280        f.write_all(content)?;
281        f.sync_all()?;
282    }
283    std::fs::rename(&tmp, p)?;
284    Ok(())
285}
286
287/// Lee /etc/machine-info, actualiza/inserta una clave, escribe atómico.
288fn update_machine_info(key: &str, value: &str) -> std::io::Result<()> {
289    let path = "/etc/machine-info";
290    let existing = std::fs::read_to_string(path).unwrap_or_default();
291    let mut found = false;
292    let mut out = String::new();
293    for line in existing.lines() {
294        if let Some((k, _)) = line.split_once('=') {
295            if k.trim() == key {
296                out.push_str(&format!("{key}={value}\n"));
297                found = true;
298                continue;
299            }
300        }
301        out.push_str(line);
302        out.push('\n');
303    }
304    if !found {
305        out.push_str(&format!("{key}={value}\n"));
306    }
307    atomic_write(path, out.as_bytes())
308}
309
310async fn announce_to_fractal() {
311    if let Ok(mut client) = BusClient::from_env().await {
312        let req = BusRequest::Announce {
313            capabilities: vec![Capability::Endpoint {
314                interface: ente_card::InterfaceId([0xa0; 16]),
315                version: 1,
316            }],
317        };
318        match client.call(req).await {
319            Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
320            Ok(other) => warn!(?other, "Announce respuesta inesperada"),
321            Err(e) => warn!(?e, "Announce falló"),
322        }
323    }
324}
325
326async fn wait_for_term() -> anyhow::Result<()> {
327    let mut term = signal(SignalKind::terminate())?;
328    let mut int_ = signal(SignalKind::interrupt())?;
329    tokio::select! {
330        _ = term.recv() => info!("SIGTERM"),
331        _ = int_.recv() => info!("SIGINT"),
332    }
333    Ok(())
334}
335
336fn init_tracing() {
337    let filter = EnvFilter::try_from_default_env()
338        .unwrap_or_else(|_| EnvFilter::new("ente_hostnamed_compat=info"));
339    tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
340}