systemd: container/remote restart, permanent-error backoff, recursive section via loader (step 6)

This commit is contained in:
Damocles 2026-05-07 21:01:23 +02:00
parent 5676b1ac62
commit dfa3840d97
6 changed files with 118 additions and 56 deletions

View file

@ -60,7 +60,13 @@ pub mod qobject {
#[qinvokable] #[qinvokable]
#[cxx_name = "restartUnit"] #[cxx_name = "restartUnit"]
fn restart_unit(self: Pin<&mut Self>, name: QString, scope: QString, machine: QString); fn restart_unit(
self: Pin<&mut Self>,
name: QString,
scope: QString,
host: QString,
machine: QString,
);
} }
impl cxx_qt::Initialize for SystemdService {} impl cxx_qt::Initialize for SystemdService {}
@ -527,13 +533,14 @@ impl qobject::SystemdService {
let applet_open = self.as_ref().rust().applet_open; let applet_open = self.as_ref().rust().applet_open;
if applet_open { if applet_open {
for target in &want_targets { for target in &want_targets {
let prev_last_seen = self let prev = self.as_ref().rust().remote_cache.get(target).cloned();
.as_ref() let prev_last_seen = prev.as_ref().map(|m| m.last_seen).unwrap_or(0);
.rust() // Backoff: don't retry hosts with a permanent error until
.remote_cache // either the config changes (cache cleared) or the user
.get(target) // restarts the shell. Reuse the cached entry verbatim.
.map(|m| m.last_seen) if prev.as_ref().is_some_and(|m| m.error_kind == "permanent") {
.unwrap_or(0); continue;
}
let entry = match fetch_via_busctl(target, &["--host", target], false, "") { let entry = match fetch_via_busctl(target, &["--host", target], false, "") {
Ok(mut m) => { Ok(mut m) => {
// Enumerate + fetch remote nspawn containers via the // Enumerate + fetch remote nspawn containers via the
@ -599,28 +606,70 @@ impl qobject::SystemdService {
} }
} }
fn restart_unit(self: Pin<&mut Self>, name: QString, scope: QString, machine: QString) { fn restart_unit(
self: Pin<&mut Self>,
name: QString,
scope: QString,
host: QString,
machine: QString,
) {
let name = name.to_string(); let name = name.to_string();
let scope = scope.to_string(); let scope = scope.to_string();
let host = host.to_string();
let machine = machine.to_string(); let machine = machine.to_string();
let _ = self; let _ = self;
rt().block_on(async move {
// Local-only restart for now. Container/remote restart is TODO. // Local + system or user scope: native zbus, supports user manager too.
if !machine.is_empty() { if host.is_empty() && machine.is_empty() {
tracing::warn!(target: "nova_plugin", machine = %machine, "container/remote restart not yet implemented"); rt().block_on(async move {
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");
}
});
return;
}
// Container or remote: spawn busctl with the matching prefix flags.
// (busctl doesn't have a concept of `--user` over remote; we restart
// system units only for non-local hosts.)
let mut prefix: Vec<String> = Vec::new();
if !host.is_empty() {
prefix.push("--host".into());
prefix.push(host.clone());
}
if !machine.is_empty() {
prefix.push("--machine".into());
prefix.push(machine.clone());
}
let mut args: Vec<&str> = prefix.iter().map(String::as_str).collect();
args.extend([
"call",
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
"RestartUnit",
"ss",
name.as_str(),
"replace",
]);
let out = std::process::Command::new("busctl").args(&args).output();
match out {
Ok(o) if o.status.success() => {}
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
tracing::warn!(target: "nova_plugin", unit = %name, host = %host, machine = %machine, error = %stderr, "restart_unit busctl failed");
} }
let conn = match scope.as_str() { Err(e) => {
"user" => Connection::session().await, tracing::warn!(target: "nova_plugin", unit = %name, error = %e, "restart_unit busctl spawn failed");
_ => 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");
} }
}); }
} }
} }

View file

@ -32,7 +32,8 @@ Column {
SystemdMachineSection { SystemdMachineSection {
width: _row.width width: _row.width
accentColor: root.accentColor accentColor: root.accentColor
machineName: _row.modelData.isLocal ? "" : _row.modelData.name hostTarget: _row.modelData.isLocal ? "" : _row.modelData.name
machineName: ""
title: _row.modelData.name title: _row.modelData.name
marker: _row.modelData.marker ?? "" marker: _row.modelData.marker ?? ""
systemState: _row.modelData.systemState ?? "unknown" systemState: _row.modelData.systemState ?? "unknown"

View file

@ -10,15 +10,17 @@ import NovaStats as NS
Column { Column {
id: root id: root
required property color accentColor property color accentColor
required property string machineName // SSH target string (for remote hosts/their containers); "" for local-side entries.
required property string title property string hostTarget: ""
required property string marker property string machineName: ""
required property string systemState property string title: ""
required property int runningCount property string marker: ""
required property int totalCount property string systemState: "unknown"
required property var failedUnits property int runningCount: 0
required property var runningUnits property int totalCount: 0
property var failedUnits: []
property var runningUnits: []
property string errorKind: "" property string errorKind: ""
property string errorReason: "" property string errorReason: ""
property int lastSeen: 0 property int lastSeen: 0
@ -169,6 +171,7 @@ Column {
description: modelData.description ?? "" description: modelData.description ?? ""
subState: modelData.subState ?? "" subState: modelData.subState ?? ""
scope: modelData.scope ?? "system" scope: modelData.scope ?? "system"
hostTarget: root.hostTarget
machineName: root.machineName machineName: root.machineName
accentColor: root.accentColor accentColor: root.accentColor
} }
@ -229,40 +232,47 @@ Column {
description: modelData.description ?? "" description: modelData.description ?? ""
subState: modelData.subState ?? "" subState: modelData.subState ?? ""
scope: modelData.scope ?? "system" scope: modelData.scope ?? "system"
hostTarget: root.hostTarget
machineName: root.machineName machineName: root.machineName
accentColor: root.accentColor accentColor: root.accentColor
} }
} }
// Nested containers running on this machine. Indented to convey hierarchy. // Nested containers running on this machine. Indented to convey hierarchy.
// QML disallows direct recursive component use, so we load the same .qml
// by source path through a Loader to break the static graph.
Repeater { Repeater {
model: root.containers ?? [] model: root.containers ?? []
delegate: Item { delegate: Item {
id: _childWrap id: _childWrap
required property var modelData required property var modelData
width: root.width width: root.width
height: _child.height + 4 height: _childLoader.height + 4
SystemdMachineSection { Loader {
id: _child id: _childLoader
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
width: parent.width - 16 width: parent.width - 16
accentColor: root.accentColor source: "SystemdMachineSection.qml"
machineName: _childWrap.modelData.name onLoaded: {
title: _childWrap.modelData.name item.accentColor = root.accentColor;
marker: _childWrap.modelData.marker ?? "" item.hostTarget = root.hostTarget;
systemState: _childWrap.modelData.systemState ?? "unknown" item.machineName = _childWrap.modelData.name;
runningCount: _childWrap.modelData.runningCount ?? 0 item.title = _childWrap.modelData.name;
totalCount: _childWrap.modelData.totalCount ?? 0 item.marker = _childWrap.modelData.marker ?? "";
failedUnits: _childWrap.modelData.failedUnits ?? [] item.systemState = _childWrap.modelData.systemState ?? "unknown";
runningUnits: _childWrap.modelData.runningUnits ?? [] item.runningCount = _childWrap.modelData.runningCount ?? 0;
errorKind: _childWrap.modelData.errorKind ?? "" item.totalCount = _childWrap.modelData.totalCount ?? 0;
errorReason: _childWrap.modelData.errorReason ?? "" item.failedUnits = _childWrap.modelData.failedUnits ?? [];
lastSeen: _childWrap.modelData.lastSeen ?? 0 item.runningUnits = _childWrap.modelData.runningUnits ?? [];
containers: _childWrap.modelData.containers ?? [] item.errorKind = _childWrap.modelData.errorKind ?? "";
depth: root.depth + 1 item.errorReason = _childWrap.modelData.errorReason ?? "";
onContentResized: root.contentResized() item.lastSeen = _childWrap.modelData.lastSeen ?? 0;
item.containers = _childWrap.modelData.containers ?? [];
item.depth = root.depth + 1;
item.contentResized.connect(root.contentResized);
}
} }
} }
} }

View file

@ -9,6 +9,7 @@ Item {
required property string description required property string description
required property string subState required property string subState
required property string scope required property string scope
required property string hostTarget
required property string machineName required property string machineName
required property color accentColor required property color accentColor
@ -96,7 +97,7 @@ Item {
} }
TapHandler { TapHandler {
enabled: _rowHover.hovered enabled: _rowHover.hovered
onTapped: S.SystemdService.restartUnit(root.unitName, root.scope, root.machineName) onTapped: S.SystemdService.restartUnit(root.unitName, root.scope, root.hostTarget, root.machineName)
} }
} }
} }

View file

@ -21,8 +21,8 @@ QtObject {
} }
} }
function restartUnit(name, scope, machine) { function restartUnit(name, scope, host, machine) {
NS.SystemdService.restartUnit(name, scope ?? "system", machine ?? ""); NS.SystemdService.restartUnit(name, scope ?? "system", host ?? "", machine ?? "");
} }
function refresh() { function refresh() {

View file

@ -70,6 +70,7 @@ shell/applets/PowerApplet.qml: Type "QColor" of property "base0D" not found. Thi
shell/applets/PowerApplet.qml: Type "QColor" of property "base0E" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type] shell/applets/PowerApplet.qml: Type "QColor" of property "base0E" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/PowerApplet.qml: Unqualified access [unqualified] shell/applets/PowerApplet.qml: Unqualified access [unqualified]
shell/applets/Separator.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type] shell/applets/Separator.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Member "contentResized" not found on type "QObject" [missing-property]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base00" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type] shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base00" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base02" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type] shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base02" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]
shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type] shell/applets/SystemdMachineSection.qml: Type "QColor" of property "base03" not found. This is likely due to a missing dependency entry or a type not being exposed declaratively. [unresolved-type]