Compare commits

..
5 changed files with 127 additions and 280 deletions

View file

@ -101,10 +101,9 @@ pub mod qobject {
#[qproperty(bool, dock_applet_mpris, cxx_name = "dockAppletMpris")] #[qproperty(bool, dock_applet_mpris, cxx_name = "dockAppletMpris")]
#[qproperty(bool, dock_applet_notifications, cxx_name = "dockAppletNotifications")] #[qproperty(bool, dock_applet_notifications, cxx_name = "dockAppletNotifications")]
#[qproperty(bool, dock_applet_power, cxx_name = "dockAppletPower")] #[qproperty(bool, dock_applet_power, cxx_name = "dockAppletPower")]
// Unified systemd bar module (covers local + nspawn containers + remotes). // Unified systemd bar module (covers local + nspawn containers, and later remotes).
#[qproperty(bool, systemd_enable, cxx_name = "systemdEnable")] #[qproperty(bool, systemd_enable, cxx_name = "systemdEnable")]
#[qproperty(i32, systemd_interval, cxx_name = "systemdInterval")] #[qproperty(i32, systemd_interval, cxx_name = "systemdInterval")]
#[qproperty(QList_QString, systemd_machines, cxx_name = "systemdMachines")]
type ModulesService = super::ModulesServiceRust; type ModulesService = super::ModulesServiceRust;
} }
@ -444,34 +443,6 @@ mod data {
} }
} }
// Systemd-bar group: enable + poll interval + remote SSH targets. Each
// `machines` entry is a string like "host" or "user@host"; the local
// machine is deduped by hostname even if listed.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Systemd {
#[serde(default = "t")]
pub enable: bool,
#[serde(default = "Systemd::d_interval")]
pub interval: i32,
#[serde(default)]
pub machines: Vec<String>,
}
impl Systemd {
fn d_interval() -> i32 {
15_000
}
}
impl Default for Systemd {
fn default() -> Self {
Self {
enable: true,
interval: Self::d_interval(),
machines: Vec::new(),
}
}
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StatsDaemon { pub struct StatsDaemon {
@ -547,7 +518,7 @@ mod data {
#[serde(default)] #[serde(default)]
pub dock: Dock, pub dock: Dock,
#[serde(default)] #[serde(default)]
pub systemd: Systemd, pub systemd: WithInterval,
#[serde(default)] #[serde(default)]
pub stats_daemon: StatsDaemon, pub stats_daemon: StatsDaemon,
} }
@ -624,7 +595,6 @@ pub struct ModulesServiceRust {
dock_applet_power: bool, dock_applet_power: bool,
systemd_enable: bool, systemd_enable: bool,
systemd_interval: i32, systemd_interval: i32,
systemd_machines: QList<QString>,
} }
impl Default for ModulesServiceRust { impl Default for ModulesServiceRust {
@ -708,13 +678,6 @@ impl ModulesServiceRust {
dock_applet_power: d.dock.applets.power, dock_applet_power: d.dock.applets.power,
systemd_enable: d.systemd.enable, systemd_enable: d.systemd.enable,
systemd_interval: d.systemd.interval, systemd_interval: d.systemd.interval,
systemd_machines: {
let mut list = QList::<QString>::default();
for s in &d.systemd.machines {
list.append(QString::from(s.as_str()));
}
list
},
} }
} }
} }
@ -728,10 +691,6 @@ pub(crate) fn config_path(file: &str) -> PathBuf {
base.join("nova-shell").join(file) base.join("nova-shell").join(file)
} }
pub(crate) fn load_systemd_machines() -> Vec<String> {
load_modules_data().systemd.machines
}
fn load_modules_data() -> ModulesData { fn load_modules_data() -> ModulesData {
let path = config_path("modules.json"); let path = config_path("modules.json");
let raw = match std::fs::read_to_string(&path) { let raw = match std::fs::read_to_string(&path) {

View file

@ -1,12 +1,12 @@
// In-process systemd state for nova-shell. // In-process systemd state for nova-shell.
// //
// The machine tree is exposed to QML as a JSON-encoded QString (`machinesJson`) // Lists of units and machines are exposed to QML as JSON-encoded QString props
// rather than QList<QVariantMap>. cxx-qt 0.8.1 does not implement QVariantValue // (`failedUnitsJson`, `containersJson`) rather than QList<QVariantMap>. cxx-qt
// for QVariantMap/QVariantList, and cxx-qt main on git regressed qt-build-utils // 0.8.1 does not implement QVariantValue for QVariantMap/QVariantList, and
// to require QuickControls2.prl files that nixpkgs strips. Switch to // cxx-qt main on git regressed qt-build-utils to require QuickControls2.prl
// QList<QVariantMap> when a release ships with both fixes. // files that nixpkgs strips. Switch to QList<QVariantMap> when a release ships
// with both fixes.
use crate::modules_service;
use core::pin::Pin; use core::pin::Pin;
use cxx_qt_lib::QString; use cxx_qt_lib::QString;
use serde::Serialize; use serde::Serialize;
@ -26,13 +26,13 @@ pub mod qobject {
#[qml_element] #[qml_element]
#[qml_singleton] #[qml_singleton]
#[qproperty(QString, hostname)] #[qproperty(QString, hostname)]
// Local failed unit count (drives the bar module label). #[qproperty(QString, system_state, cxx_name = "systemState")]
#[qproperty(QString, user_state, cxx_name = "userState")]
#[qproperty(i32, failed_count, cxx_name = "failedCount")] #[qproperty(i32, failed_count, cxx_name = "failedCount")]
// JSON array, local first then nspawn containers. Each entry: // JSON array: [{ name, description, subState, scope: "system"|"user", machine: "" | name }]
// { name, isLocal, marker, systemState, runningCount, totalCount, #[qproperty(QString, failed_units_json, cxx_name = "failedUnitsJson")]
// failedUnits: [{name, description, subState, scope, machine}], // JSON array: [{ name, class, service, systemState, failedUnits: [...] }]
// runningUnits: [...] } #[qproperty(QString, containers_json, cxx_name = "containersJson")]
#[qproperty(QString, machines_json, cxx_name = "machinesJson")]
type SystemdService = super::SystemdServiceRust; type SystemdService = super::SystemdServiceRust;
#[qinvokable] #[qinvokable]
@ -46,7 +46,7 @@ pub mod qobject {
impl cxx_qt::Initialize for SystemdService {} impl cxx_qt::Initialize for SystemdService {}
} }
// systemd1.Manager.ListUnits returns a(ssssssouso): name, description, // systemd1.Manager.ListUnitsFiltered returns a(ssssssouso): name, description,
// load_state, active_state, sub_state, follower, unit_path, job_id, job_type, job_path. // load_state, active_state, sub_state, follower, unit_path, job_id, job_type, job_path.
type UnitTuple = ( type UnitTuple = (
String, String,
@ -70,7 +70,7 @@ trait SystemdManager {
#[zbus(property)] #[zbus(property)]
fn system_state(&self) -> zbus::Result<String>; fn system_state(&self) -> zbus::Result<String>;
fn list_units(&self) -> zbus::Result<Vec<UnitTuple>>; fn list_units_filtered(&self, states: Vec<&str>) -> zbus::Result<Vec<UnitTuple>>;
fn restart_unit(&self, name: &str, mode: &str) fn restart_unit(&self, name: &str, mode: &str)
-> zbus::Result<zbus::zvariant::OwnedObjectPath>; -> zbus::Result<zbus::zvariant::OwnedObjectPath>;
@ -88,45 +88,44 @@ trait Machined {
} }
#[derive(Serialize)] #[derive(Serialize)]
struct UnitJson { struct UnitJson<'a> {
name: String, name: &'a str,
description: String, description: &'a str,
#[serde(rename = "subState")] #[serde(rename = "subState")]
sub_state: String, sub_state: &'a str,
scope: String, scope: &'a str,
machine: String, machine: &'a str,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct MachineJson { struct ContainerJson<'a> {
name: String, name: &'a str,
#[serde(rename = "isLocal")] class: &'a str,
is_local: bool, service: &'a str,
marker: String,
#[serde(rename = "systemState")] #[serde(rename = "systemState")]
system_state: String, system_state: &'a str,
#[serde(rename = "runningCount")]
running_count: i32,
#[serde(rename = "totalCount")]
total_count: i32,
#[serde(rename = "failedUnits")] #[serde(rename = "failedUnits")]
failed_units: Vec<UnitJson>, failed_units: Vec<UnitJson<'a>>,
#[serde(rename = "runningUnits")]
running_units: Vec<UnitJson>,
} }
pub struct SystemdServiceRust { pub struct SystemdServiceRust {
hostname: QString, hostname: QString,
system_state: QString,
user_state: QString,
failed_count: i32, failed_count: i32,
machines_json: QString, failed_units_json: QString,
containers_json: QString,
} }
impl Default for SystemdServiceRust { impl Default for SystemdServiceRust {
fn default() -> Self { fn default() -> Self {
Self { Self {
hostname: QString::from(read_hostname()), hostname: QString::from(read_hostname()),
system_state: QString::from("unknown"),
user_state: QString::from("unknown"),
failed_count: 0, failed_count: 0,
machines_json: QString::from("[]"), failed_units_json: QString::from("[]"),
containers_json: QString::from("[]"),
} }
} }
} }
@ -147,47 +146,22 @@ fn rt() -> &'static Runtime {
}) })
} }
async fn fetch_units(bus: &Connection) -> (String, Vec<UnitTuple>) { async fn fetch_failed(bus: &Connection) -> (String, Vec<UnitTuple>) {
let mut state = String::from("unknown"); let mut state = String::from("unknown");
let mut units = Vec::new(); let mut units = Vec::new();
if let Ok(mgr) = SystemdManagerProxy::new(bus).await { if let Ok(mgr) = SystemdManagerProxy::new(bus).await {
if let Ok(s) = mgr.system_state().await { if let Ok(s) = mgr.system_state().await {
state = s; state = s;
} }
if let Ok(u) = mgr.list_units().await { if let Ok(u) = mgr.list_units_filtered(vec!["failed"]).await {
units = u; units = u;
} }
} }
(state, units) (state, units)
} }
// Partition a unit list by active_state. (failed, running)
fn partition_units(
units: Vec<UnitTuple>,
scope: &str,
machine: &str,
) -> (Vec<UnitJson>, Vec<UnitJson>, i32) {
let total = units.len() as i32;
let mut failed = Vec::new();
let mut running = Vec::new();
for u in units {
let entry = UnitJson {
name: u.0,
description: u.1,
sub_state: u.4,
scope: scope.to_string(),
machine: machine.to_string(),
};
match u.3.as_str() {
"failed" => failed.push(entry),
"active" => running.push(entry),
_ => {}
}
}
(failed, running, total)
}
async fn poll_async() -> ( async fn poll_async() -> (
String,
String, String,
Vec<UnitTuple>, Vec<UnitTuple>,
Vec<UnitTuple>, Vec<UnitTuple>,
@ -195,11 +169,12 @@ async fn poll_async() -> (
) { ) {
let mut sys_state = String::from("unknown"); let mut sys_state = String::from("unknown");
let mut sys_units = Vec::new(); let mut sys_units = Vec::new();
let mut user_state = String::from("unknown");
let mut user_units = Vec::new(); let mut user_units = Vec::new();
let mut machines = Vec::new(); let mut machines = Vec::new();
if let Ok(c) = Connection::system().await { if let Ok(c) = Connection::system().await {
let (s, u) = fetch_units(&c).await; let (s, u) = fetch_failed(&c).await;
sys_state = s; sys_state = s;
sys_units = u; sys_units = u;
if let Ok(m) = MachinedProxy::new(&c).await { if let Ok(m) = MachinedProxy::new(&c).await {
@ -213,11 +188,25 @@ async fn poll_async() -> (
} }
} }
if let Ok(c) = Connection::session().await { if let Ok(c) = Connection::session().await {
let (_, u) = fetch_units(&c).await; let (s, u) = fetch_failed(&c).await;
user_state = s;
user_units = u; user_units = u;
} }
(sys_state, sys_units, user_units, machines) (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 { impl cxx_qt::Initialize for qobject::SystemdService {
@ -228,76 +217,33 @@ impl cxx_qt::Initialize for qobject::SystemdService {
impl qobject::SystemdService { impl qobject::SystemdService {
fn poll(mut self: Pin<&mut Self>) { fn poll(mut self: Pin<&mut Self>) {
let (sys_state, sys_units, user_units, machines) = rt().block_on(poll_async()); let (sys_state, user_state, sys_units, user_units, machines) = rt().block_on(poll_async());
let (sys_failed, sys_running, sys_total) = partition_units(sys_units, "system", ""); let mut all_failed: Vec<UnitJson> = Vec::with_capacity(sys_units.len() + user_units.len());
let (user_failed, user_running, user_total) = partition_units(user_units, "user", ""); 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 mut failed: Vec<UnitJson> = Vec::with_capacity(sys_failed.len() + user_failed.len()); let containers: Vec<ContainerJson> = machines
failed.extend(sys_failed); .iter()
failed.extend(user_failed); .map(|(n, c, s)| ContainerJson {
let mut running: Vec<UnitJson> = Vec::with_capacity(sys_running.len() + user_running.len()); name: n,
running.extend(sys_running); class: c,
running.extend(user_running); service: s,
system_state: "unknown",
let local_failed_count = failed.len() as i32;
let local_running_count = running.len() as i32;
let local_total_count = sys_total + user_total;
let local = MachineJson {
name: read_hostname(),
is_local: true,
marker: "this machine".into(),
system_state: sys_state,
running_count: local_running_count,
total_count: local_total_count,
failed_units: failed,
running_units: running,
};
// Containers: enumerate only; unit fetching for containers comes in step 5.
let mut all_machines = Vec::with_capacity(1 + machines.len());
all_machines.push(local);
for (name, _class, _service) in &machines {
all_machines.push(MachineJson {
name: name.clone(),
is_local: false,
marker: String::new(),
system_state: "unknown".into(),
running_count: 0,
total_count: 0,
failed_units: Vec::new(), failed_units: Vec::new(),
running_units: Vec::new(), })
}); .collect();
} let containers_json = serde_json::to_string(&containers).unwrap_or_else(|_| "[]".into());
// Configured remote machines (placeholders; transport lands in step 4). self.as_mut().set_system_state(QString::from(sys_state));
// Dedup local: drop entries matching the local hostname or `localhost`, self.as_mut().set_user_state(QString::from(user_state));
// with or without a `user@` prefix.
let host = read_hostname();
let cfg_machines = modules_service::load_systemd_machines();
for target in cfg_machines {
let host_part = target.rsplit_once('@').map_or(target.as_str(), |(_, h)| h);
if host_part == host || host_part == "localhost" {
continue;
}
all_machines.push(MachineJson {
name: target.clone(),
is_local: false,
marker: String::new(),
system_state: "pending".into(),
running_count: 0,
total_count: 0,
failed_units: Vec::new(),
running_units: Vec::new(),
});
}
let machines_json = serde_json::to_string(&all_machines).unwrap_or_else(|_| "[]".into());
self.as_mut().set_failed_count(local_failed_count);
self.as_mut() self.as_mut()
.set_machines_json(QString::from(machines_json)); .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) { fn restart_unit(self: Pin<&mut Self>, name: QString, scope: QString, machine: QString) {

View file

@ -13,31 +13,38 @@ Column {
onHeightChanged: root.contentResized() onHeightChanged: root.contentResized()
// Local machine header + units
SystemdMachineSection {
width: root.width
accentColor: root.accentColor
machineName: ""
title: S.SystemdService.hostname
marker: " this machine"
systemState: S.SystemdService.systemState
units: S.SystemdService.failedUnits
startExpanded: true
}
// Containers
Repeater { Repeater {
model: S.SystemdService.machines model: S.SystemdService.containers
delegate: Column { delegate: Column {
id: _row id: _row
required property var modelData required property var modelData
required property int index
width: root.width width: root.width
Separator { Separator {}
visible: _row.index > 0
}
SystemdMachineSection { SystemdMachineSection {
width: _row.width width: _row.width
accentColor: root.accentColor accentColor: root.accentColor
machineName: _row.modelData.isLocal ? "" : _row.modelData.name machineName: _row.modelData.name
title: _row.modelData.name title: _row.modelData.name
marker: _row.modelData.marker ?? "" marker: ""
systemState: _row.modelData.systemState ?? "unknown" systemState: _row.modelData.systemState ?? "unknown"
runningCount: _row.modelData.runningCount ?? 0 units: _row.modelData.failedUnits ?? []
totalCount: _row.modelData.totalCount ?? 0 startExpanded: (_row.modelData.failedUnits ?? []).length > 0
failedUnits: _row.modelData.failedUnits ?? []
runningUnits: _row.modelData.runningUnits ?? []
onContentResized: root.contentResized()
} }
} }
} }

View file

@ -4,9 +4,8 @@ import QtQuick
import "../services" as S import "../services" as S
import NovaStats as NS import NovaStats as NS
// One section of the systemd applet: a header with title, aggregate counts, // One section of the systemd applet: a header with title + state chip,
// state chip; an auto-expanded list of failed units (hidden when empty); a // expandable list of failed units underneath.
// lazy-loaded, collapsed-by-default list of running units.
Column { Column {
id: root id: root
@ -15,21 +14,13 @@ Column {
required property string title required property string title
required property string marker required property string marker
required property string systemState required property string systemState
required property int runningCount required property var units
required property int totalCount property bool startExpanded: false
required property var failedUnits
required property var runningUnits
signal contentResized
onHeightChanged: root.contentResized()
width: parent?.width ?? 0 width: parent?.width ?? 0
property bool _runningExpanded: false property bool _expanded: startExpanded
readonly property int _failedCount: (failedUnits ?? []).length
// Header
Item { Item {
width: root.width width: root.width
height: 32 height: 32
@ -60,29 +51,10 @@ Column {
elide: Text.ElideRight elide: Text.ElideRight
} }
// Aggregate counts: "n running, m/total failed" or "n running" if no failures.
Text {
id: _counts
anchors.right: _stateChip.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: {
if (root.totalCount === 0)
return "";
const r = root.runningCount + " running";
if (root._failedCount > 0)
return r + ", " + root._failedCount + "/" + root.totalCount + " failed";
return r;
}
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 3
font.family: NS.ThemeService.fontFamily
}
Rectangle { Rectangle {
id: _stateChip id: _stateChip
anchors.right: parent.right anchors.right: _chevron.left
anchors.rightMargin: 12 anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.systemState !== "unknown" visible: root.systemState !== "unknown"
color: { color: {
@ -91,8 +63,6 @@ Column {
return NS.ThemeService.base0B; return NS.ThemeService.base0B;
if (st === "degraded") if (st === "degraded")
return NS.ThemeService.base0A; return NS.ThemeService.base0A;
if (st === "pending")
return NS.ThemeService.base04;
return NS.ThemeService.base08; return NS.ThemeService.base08;
} }
opacity: 0.85 opacity: 0.85
@ -109,71 +79,25 @@ Column {
font.family: NS.ThemeService.fontFamily font.family: NS.ThemeService.fontFamily
} }
} }
}
// Failed units (auto-expanded; entire block hidden when there are none).
Repeater {
model: root._failedCount > 0 ? root.failedUnits : []
delegate: SystemdUnitRow {
required property var modelData
unitName: modelData.name
description: modelData.description ?? ""
subState: modelData.subState ?? ""
scope: modelData.scope ?? "system"
machineName: root.machineName
accentColor: root.accentColor
}
}
// Running units toggle row (only meaningful when there are running units to show).
Item {
visible: (root.runningUnits ?? []).length > 0
width: root.width
height: 26
Rectangle {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: _runHdrHover.hovered ? NS.ThemeService.base02 : "transparent"
radius: NS.ThemeService.radius
z: -1
}
HoverHandler {
id: _runHdrHover
}
Text {
anchors.left: parent.left
anchors.leftMargin: 24
anchors.verticalCenter: parent.verticalCenter
text: "running units (" + (root.runningUnits ?? []).length + ")"
color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.fontFamily
font.letterSpacing: 1
}
Text { Text {
id: _chevron
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 12 anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root._runningExpanded ? "" : "" text: root._expanded ? "" : ""
color: NS.ThemeService.base04 color: NS.ThemeService.base04
font.pixelSize: NS.ThemeService.fontSize - 2 font.pixelSize: NS.ThemeService.fontSize - 2
font.family: NS.ThemeService.iconFontFamily font.family: NS.ThemeService.iconFontFamily
} }
TapHandler { TapHandler {
onTapped: root._runningExpanded = !root._runningExpanded onTapped: root._expanded = !root._expanded
} }
} }
// Lazy-loaded running units list. Repeater materializes rows only when the
// model is non-empty, so feeding `[]` while collapsed avoids per-row cost.
Repeater { Repeater {
model: root._runningExpanded ? root.runningUnits : [] model: root._expanded ? root.units : []
delegate: SystemdUnitRow { delegate: SystemdUnitRow {
required property var modelData required property var modelData
unitName: modelData.name unitName: modelData.name

View file

@ -4,18 +4,29 @@ import QtQuick
import NovaStats as NS import NovaStats as NS
// Thin wrapper around NS.SystemdService: drives the poll Timer and parses the // Thin wrapper around NS.SystemdService: drives the poll Timer and parses the
// JSON-encoded machine tree into a JS array for QML consumers. // JSON-encoded list properties into JS arrays for QML consumers. Restart helper
// proxies through to the Rust singleton.
QtObject { QtObject {
id: root id: root
readonly property string hostname: NS.SystemdService.hostname readonly property string hostname: NS.SystemdService.hostname
readonly property string systemState: NS.SystemdService.systemState
readonly property string userState: NS.SystemdService.userState
readonly property int failedCount: NS.SystemdService.failedCount readonly property int failedCount: NS.SystemdService.failedCount
// [{ name, isLocal, marker, systemState, runningCount, totalCount, // Parsed [{ name, description, subState, scope, machine }, ...]
// failedUnits: [...], runningUnits: [...] }, ...] readonly property var failedUnits: {
readonly property var machines: {
try { try {
return JSON.parse(NS.SystemdService.machinesJson); return JSON.parse(NS.SystemdService.failedUnitsJson);
} catch (e) {
return [];
}
}
// Parsed [{ name, class, service, systemState, failedUnits: [...] }, ...]
readonly property var containers: {
try {
return JSON.parse(NS.SystemdService.containersJson);
} catch (e) { } catch (e) {
return []; return [];
} }