273 lines
8.4 KiB
Rust
273 lines
8.4 KiB
Rust
// 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<QVariantMap>. 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<QVariantMap> 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<String>;
|
|
|
|
fn list_units_filtered(&self, states: Vec<&str>) -> zbus::Result<Vec<UnitTuple>>;
|
|
|
|
fn restart_unit(&self, name: &str, mode: &str)
|
|
-> zbus::Result<zbus::zvariant::OwnedObjectPath>;
|
|
}
|
|
|
|
#[proxy(
|
|
interface = "org.freedesktop.machine1.Manager",
|
|
default_service = "org.freedesktop.machine1",
|
|
default_path = "/org/freedesktop/machine1"
|
|
)]
|
|
trait Machined {
|
|
fn list_machines(
|
|
&self,
|
|
) -> zbus::Result<Vec<(String, String, String, zbus::zvariant::OwnedObjectPath)>>;
|
|
}
|
|
|
|
#[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<UnitJson<'a>>,
|
|
}
|
|
|
|
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<Runtime> = 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<UnitTuple>) {
|
|
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<UnitTuple>,
|
|
Vec<UnitTuple>,
|
|
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<UnitJson<'a>> {
|
|
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<UnitJson> = 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<ContainerJson> = 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");
|
|
}
|
|
});
|
|
}
|
|
}
|