⎇ ef3d698c4b4d0c0ab88da4d59c37d57fef894ce8
crates/compat/arje-hostnamed-compat/src/main.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(compat): arje-compat-common — núcleo puro y testeado para los shims
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}