gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

⎇ ef3d698c4b4d0c0ab88da4d59c37d57fef894ce8 crates/compat/arje-hostnamed-compat/src/main.rs Rust · 285 siq'ikuna · 9.9 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(compat): arje-compat-common — núcleo puro y testeado para los shims 8f946b449 · 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 arje_bus::{BusClient, BusRequest, BusResponse};
12use arje_card::Capability;
13use arje_compat_common::{atomic_write, is_valid_hostname, merge_kv, parse_kv};
14use std::sync::Mutex;
15use tokio::signal::unix::{signal, SignalKind};
16use tracing::{info, warn};
17use tracing_subscriber::EnvFilter;
18use zbus::{fdo, interface};
19
20const BUS_NAME: &str = "org.freedesktop.hostname1";
21const OBJ_PATH: &str = "/org/freedesktop/hostname1";
22
23#[tokio::main(flavor = "current_thread")]
24async fn main() -> anyhow::Result<()> {
25    init_tracing();
26    info!("ente-hostnamed-compat: arrancando");
27    announce_to_fractal().await;
28
29    let manager = HostnameManager::default();
30    let conn_result = zbus::connection::Builder::system()
31        .and_then(|b| b.name(BUS_NAME))
32        .and_then(|b| b.serve_at(OBJ_PATH, manager));
33    match conn_result {
34        Ok(builder) => match builder.build().await {
35            Ok(_conn) => {
36                info!(name = BUS_NAME, "name acquired, sirviendo");
37                wait_for_term().await
38            }
39            Err(e) => {
40                warn!(?e, "build conn falló — modo idle");
41                wait_for_term().await
42            }
43        },
44        Err(e) => {
45            warn!(?e, "builder D-Bus falló — modo idle");
46            wait_for_term().await
47        }
48    }
49}
50
51#[derive(Default)]
52struct HostnameManager {
53    /// Cache para SetHostname. En el stub no persistimos a /etc.
54    transient_hostname: Mutex<Option<String>>,
55}
56
57#[interface(name = "org.freedesktop.hostname1")]
58impl HostnameManager {
59    // ----- Properties read-only -----
60
61    #[zbus(property)]
62    async fn hostname(&self) -> String {
63        if let Some(h) = self.transient_hostname.lock().unwrap().clone() {
64            return h;
65        }
66        gethostname_libc().unwrap_or_else(|| "localhost".into())
67    }
68
69    #[zbus(property)]
70    async fn static_hostname(&self) -> String {
71        std::fs::read_to_string("/etc/hostname")
72            .map(|s| s.trim().to_string())
73            .unwrap_or_default()
74    }
75
76    #[zbus(property)]
77    async fn pretty_hostname(&self) -> String {
78        read_machine_info_field("PRETTY_HOSTNAME").unwrap_or_default()
79    }
80
81    #[zbus(property)]
82    async fn icon_name(&self) -> String {
83        read_machine_info_field("ICON_NAME").unwrap_or_default()
84    }
85
86    #[zbus(property)]
87    async fn chassis(&self) -> String {
88        read_machine_info_field("CHASSIS").unwrap_or_else(|| "desktop".into())
89    }
90
91    #[zbus(property)]
92    async fn deployment(&self) -> String {
93        read_machine_info_field("DEPLOYMENT").unwrap_or_default()
94    }
95
96    #[zbus(property)]
97    async fn location(&self) -> String {
98        read_machine_info_field("LOCATION").unwrap_or_default()
99    }
100
101    #[zbus(property)]
102    async fn kernel_name(&self) -> String {
103        nix::sys::utsname::uname()
104            .ok()
105            .and_then(|u| u.sysname().to_str().map(String::from))
106            .unwrap_or_else(|| "Linux".into())
107    }
108
109    #[zbus(property)]
110    async fn kernel_release(&self) -> String {
111        nix::sys::utsname::uname()
112            .ok()
113            .and_then(|u| u.release().to_str().map(String::from))
114            .unwrap_or_default()
115    }
116
117    #[zbus(property)]
118    async fn kernel_version(&self) -> String {
119        nix::sys::utsname::uname()
120            .ok()
121            .and_then(|u| u.version().to_str().map(String::from))
122            .unwrap_or_default()
123    }
124
125    #[zbus(property)]
126    async fn operating_system_pretty_name(&self) -> String {
127        read_os_release_field("PRETTY_NAME").unwrap_or_else(|| "Linux".into())
128    }
129
130    #[zbus(property)]
131    async fn operating_system_cpename(&self) -> String {
132        read_os_release_field("CPE_NAME").unwrap_or_default()
133    }
134
135    #[zbus(property)]
136    async fn home_url(&self) -> String {
137        read_os_release_field("HOME_URL").unwrap_or_default()
138    }
139
140    #[zbus(property)]
141    async fn hardware_vendor(&self) -> String {
142        read_dmi("/sys/class/dmi/id/sys_vendor")
143    }
144
145    #[zbus(property)]
146    async fn hardware_model(&self) -> String {
147        read_dmi("/sys/class/dmi/id/product_name")
148    }
149
150    #[zbus(property)]
151    async fn firmware_version(&self) -> String {
152        read_dmi("/sys/class/dmi/id/bios_version")
153    }
154
155    // ----- Setters: forward al bus interno y guardan en cache -----
156
157    async fn set_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
158        if !is_valid_hostname(&name) {
159            return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}")));
160        }
161        // sethostname(2) cambia sólo el running kernel value.
162        let cstr = std::ffi::CString::new(name.clone())
163            .map_err(|e| fdo::Error::Failed(format!("CString: {e}")))?;
164        let r = unsafe { libc::sethostname(cstr.as_ptr(), name.len()) };
165        if r != 0 {
166            warn!(error = %std::io::Error::last_os_error(), %name, "sethostname syscall falló (¿CAP_SYS_ADMIN?)");
167            // No es fatal — guardamos transient para que el property lea el valor nuevo.
168        }
169        *self.transient_hostname.lock().unwrap() = Some(name.clone());
170        info!(%name, "SetHostname aplicado");
171        Ok(())
172    }
173
174    async fn set_static_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
175        if !is_valid_hostname(&name) {
176            return Err(fdo::Error::InvalidArgs(format!("hostname inválido: {name:?}")));
177        }
178        atomic_write("/etc/hostname", format!("{name}\n").as_bytes())
179            .map_err(|e| fdo::Error::Failed(format!("write /etc/hostname: {e}")))?;
180        info!(%name, "SetStaticHostname → /etc/hostname");
181        Ok(())
182    }
183
184    async fn set_pretty_hostname(&self, name: String, _interactive: bool) -> fdo::Result<()> {
185        update_machine_info("PRETTY_HOSTNAME", &name)
186            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
187        info!(%name, "SetPrettyHostname → /etc/machine-info");
188        Ok(())
189    }
190
191    async fn set_icon_name(&self, name: String, _interactive: bool) -> fdo::Result<()> {
192        update_machine_info("ICON_NAME", &name)
193            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
194        info!(%name, "SetIconName → /etc/machine-info");
195        Ok(())
196    }
197
198    async fn set_chassis(&self, chassis: String, _interactive: bool) -> fdo::Result<()> {
199        if !matches!(chassis.as_str(), "desktop"|"laptop"|"server"|"tablet"|"handset"|"watch"|"embedded"|"vm"|"container") {
200            return Err(fdo::Error::InvalidArgs(format!("chassis inválido: {chassis}")));
201        }
202        update_machine_info("CHASSIS", &chassis)
203            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
204        info!(%chassis, "SetChassis → /etc/machine-info");
205        Ok(())
206    }
207
208    async fn set_deployment(&self, deployment: String, _interactive: bool) -> fdo::Result<()> {
209        update_machine_info("DEPLOYMENT", &deployment)
210            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
211        info!(%deployment, "SetDeployment → /etc/machine-info");
212        Ok(())
213    }
214
215    async fn set_location(&self, location: String, _interactive: bool) -> fdo::Result<()> {
216        update_machine_info("LOCATION", &location)
217            .map_err(|e| fdo::Error::Failed(format!("machine-info: {e}")))?;
218        info!(%location, "SetLocation → /etc/machine-info");
219        Ok(())
220    }
221}
222
223// ---------------- helpers ----------------
224
225fn gethostname_libc() -> Option<String> {
226    let mut buf = [0u8; 256];
227    let r = unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) };
228    if r != 0 { return None; }
229    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
230    std::str::from_utf8(&buf[..len]).ok().map(String::from)
231}
232
233fn read_os_release_field(field: &str) -> Option<String> {
234    parse_kv(&std::fs::read_to_string("/etc/os-release").ok()?, field)
235}
236
237fn read_machine_info_field(field: &str) -> Option<String> {
238    parse_kv(&std::fs::read_to_string("/etc/machine-info").ok()?, field)
239}
240
241fn read_dmi(path: &str) -> String {
242    std::fs::read_to_string(path)
243        .map(|s| s.trim().to_string())
244        .unwrap_or_default()
245}
246
247/// Lee `/etc/machine-info`, actualiza/inserta una clave y reescribe el
248/// archivo de forma atómica.
249fn update_machine_info(key: &str, value: &str) -> std::io::Result<()> {
250    let path = "/etc/machine-info";
251    let existing = std::fs::read_to_string(path).unwrap_or_default();
252    atomic_write(path, merge_kv(&existing, key, value).as_bytes())
253}
254
255async fn announce_to_fractal() {
256    if let Ok(mut client) = BusClient::from_env().await {
257        let req = BusRequest::Announce {
258            capabilities: vec![Capability::Endpoint {
259                interface: arje_card::InterfaceId([0xa0; 16]),
260                version: 1,
261            }],
262        };
263        match client.call(req).await {
264            Ok(BusResponse::Ok) => info!("Announce → bus interno OK"),
265            Ok(other) => warn!(?other, "Announce respuesta inesperada"),
266            Err(e) => warn!(?e, "Announce falló"),
267        }
268    }
269}
270
271async fn wait_for_term() -> anyhow::Result<()> {
272    let mut term = signal(SignalKind::terminate())?;
273    let mut int_ = signal(SignalKind::interrupt())?;
274    tokio::select! {
275        _ = term.recv() => info!("SIGTERM"),
276        _ = int_.recv() => info!("SIGINT"),
277    }
278    Ok(())
279}
280
281fn init_tracing() {
282    let filter = EnvFilter::try_from_default_env()
283        .unwrap_or_else(|_| EnvFilter::new("arje_hostnamed_compat=info"));
284    tracing_subscriber::fmt().with_env_filter(filter).with_target(true).init();
285}