// In-process systemd state for nova-shell. // // Lists of units and machines are exposed to QML as JSON-encoded QString props // (`failedUnitsJson`, `containersJson`) rather than QList. cxx-qt // 0.8.1 does not implement QVariantValue for QVariantMap/QVariantList, and // cxx-qt main on git regressed qt-build-utils to require QuickControls2.prl // files that nixpkgs strips. Switch to QList when a release ships // with both fixes. use core::pin::Pin; use cxx_qt_lib::QString; use serde::Serialize; use std::sync::OnceLock; use tokio::runtime::Runtime; use zbus::{Connection, proxy}; #[cxx_qt::bridge] pub mod qobject { unsafe extern "C++" { include!("cxx-qt-lib/qstring.h"); type QString = cxx_qt_lib::QString; } extern "RustQt" { #[qobject] #[qml_element] #[qml_singleton] #[qproperty(QString, hostname)] #[qproperty(QString, system_state, cxx_name = "systemState")] #[qproperty(QString, user_state, cxx_name = "userState")] #[qproperty(i32, failed_count, cxx_name = "failedCount")] // JSON array: [{ name, description, subState, scope: "system"|"user", machine: "" | name }] #[qproperty(QString, failed_units_json, cxx_name = "failedUnitsJson")] // JSON array: [{ name, class, service, systemState, failedUnits: [...] }] #[qproperty(QString, containers_json, cxx_name = "containersJson")] type SystemdService = super::SystemdServiceRust; #[qinvokable] fn poll(self: Pin<&mut Self>); #[qinvokable] #[cxx_name = "restartUnit"] fn restart_unit(self: Pin<&mut Self>, name: QString, scope: QString, machine: QString); } impl cxx_qt::Initialize for SystemdService {} } // systemd1.Manager.ListUnitsFiltered returns a(ssssssouso): name, description, // load_state, active_state, sub_state, follower, unit_path, job_id, job_type, job_path. type UnitTuple = ( String, String, String, String, String, String, zbus::zvariant::OwnedObjectPath, u32, String, zbus::zvariant::OwnedObjectPath, ); #[proxy( interface = "org.freedesktop.systemd1.Manager", default_service = "org.freedesktop.systemd1", default_path = "/org/freedesktop/systemd1" )] trait SystemdManager { #[zbus(property)] fn system_state(&self) -> zbus::Result; fn list_units_filtered(&self, states: Vec<&str>) -> zbus::Result>; fn restart_unit(&self, name: &str, mode: &str) -> zbus::Result; } #[proxy( interface = "org.freedesktop.machine1.Manager", default_service = "org.freedesktop.machine1", default_path = "/org/freedesktop/machine1" )] trait Machined { fn list_machines( &self, ) -> zbus::Result>; } #[derive(Serialize)] struct UnitJson<'a> { name: &'a str, description: &'a str, #[serde(rename = "subState")] sub_state: &'a str, scope: &'a str, machine: &'a str, } #[derive(Serialize)] struct ContainerJson<'a> { name: &'a str, class: &'a str, service: &'a str, #[serde(rename = "systemState")] system_state: &'a str, #[serde(rename = "failedUnits")] failed_units: Vec>, } pub struct SystemdServiceRust { hostname: QString, system_state: QString, user_state: QString, failed_count: i32, failed_units_json: QString, containers_json: QString, } impl Default for SystemdServiceRust { fn default() -> Self { Self { hostname: QString::from(read_hostname()), system_state: QString::from("unknown"), user_state: QString::from("unknown"), failed_count: 0, failed_units_json: QString::from("[]"), containers_json: QString::from("[]"), } } } fn read_hostname() -> String { std::fs::read_to_string("/etc/hostname") .map(|s| s.trim().to_string()) .unwrap_or_else(|_| "localhost".to_string()) } fn rt() -> &'static Runtime { static RT: OnceLock = OnceLock::new(); RT.get_or_init(|| { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("tokio runtime") }) } async fn fetch_failed(bus: &Connection) -> (String, Vec) { let mut state = String::from("unknown"); let mut units = Vec::new(); if let Ok(mgr) = SystemdManagerProxy::new(bus).await { if let Ok(s) = mgr.system_state().await { state = s; } if let Ok(u) = mgr.list_units_filtered(vec!["failed"]).await { units = u; } } (state, units) } async fn poll_async() -> ( String, String, Vec, Vec, Vec<(String, String, String)>, ) { let mut sys_state = String::from("unknown"); let mut sys_units = Vec::new(); let mut user_state = String::from("unknown"); let mut user_units = Vec::new(); let mut machines = Vec::new(); if let Ok(c) = Connection::system().await { let (s, u) = fetch_failed(&c).await; sys_state = s; sys_units = u; if let Ok(m) = MachinedProxy::new(&c).await { if let Ok(list) = m.list_machines().await { machines = list .into_iter() .filter(|(name, _, _, _)| name != ".host") .map(|(n, c, s, _)| (n, c, s)) .collect(); } } } if let Ok(c) = Connection::session().await { let (s, u) = fetch_failed(&c).await; user_state = s; user_units = u; } (sys_state, user_state, sys_units, user_units, machines) } fn unit_jsons<'a>(units: &'a [UnitTuple], scope: &'a str, machine: &'a str) -> Vec> { units .iter() .map(|u| UnitJson { name: &u.0, description: &u.1, sub_state: &u.4, scope, machine, }) .collect() } impl cxx_qt::Initialize for qobject::SystemdService { fn initialize(self: Pin<&mut Self>) { self.poll(); } } impl qobject::SystemdService { fn poll(mut self: Pin<&mut Self>) { let (sys_state, user_state, sys_units, user_units, machines) = rt().block_on(poll_async()); let mut all_failed: Vec = Vec::with_capacity(sys_units.len() + user_units.len()); all_failed.extend(unit_jsons(&sys_units, "system", "")); all_failed.extend(unit_jsons(&user_units, "user", "")); let count = all_failed.len() as i32; let failed_json = serde_json::to_string(&all_failed).unwrap_or_else(|_| "[]".into()); let containers: Vec = machines .iter() .map(|(n, c, s)| ContainerJson { name: n, class: c, service: s, system_state: "unknown", failed_units: Vec::new(), }) .collect(); let containers_json = serde_json::to_string(&containers).unwrap_or_else(|_| "[]".into()); self.as_mut().set_system_state(QString::from(sys_state)); self.as_mut().set_user_state(QString::from(user_state)); self.as_mut() .set_failed_units_json(QString::from(failed_json)); self.as_mut().set_failed_count(count); self.as_mut() .set_containers_json(QString::from(containers_json)); } fn restart_unit(self: Pin<&mut Self>, name: QString, scope: QString, machine: QString) { let name = name.to_string(); let scope = scope.to_string(); let machine = machine.to_string(); let _ = self; rt().block_on(async move { // Local-only restart for now. Container/remote restart comes later. if !machine.is_empty() { tracing::warn!(target: "nova_plugin", machine = %machine, "container restart not yet implemented"); return; } let conn = match scope.as_str() { "user" => Connection::session().await, _ => Connection::system().await, }; let Ok(conn) = conn else { return }; let Ok(mgr) = SystemdManagerProxy::new(&conn).await else { return; }; if let Err(e) = mgr.restart_unit(&name, "replace").await { tracing::warn!(target: "nova_plugin", unit = %name, error = %e, "restart_unit failed"); } }); } }