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]
#[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 {}
@ -527,13 +533,14 @@ impl qobject::SystemdService {
let applet_open = self.as_ref().rust().applet_open;
if applet_open {
for target in &want_targets {
let prev_last_seen = self
.as_ref()
.rust()
.remote_cache
.get(target)
.map(|m| m.last_seen)
.unwrap_or(0);
let prev = self.as_ref().rust().remote_cache.get(target).cloned();
let prev_last_seen = prev.as_ref().map(|m| m.last_seen).unwrap_or(0);
// Backoff: don't retry hosts with a permanent error until
// either the config changes (cache cleared) or the user
// restarts the shell. Reuse the cached entry verbatim.
if prev.as_ref().is_some_and(|m| m.error_kind == "permanent") {
continue;
}
let entry = match fetch_via_busctl(target, &["--host", target], false, "") {
Ok(mut m) => {
// Enumerate + fetch remote nspawn containers via the
@ -599,17 +606,22 @@ 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 scope = scope.to_string();
let host = host.to_string();
let machine = machine.to_string();
let _ = self;
// Local + system or user scope: native zbus, supports user manager too.
if host.is_empty() && machine.is_empty() {
rt().block_on(async move {
// Local-only restart for now. Container/remote restart is TODO.
if !machine.is_empty() {
tracing::warn!(target: "nova_plugin", machine = %machine, "container/remote restart not yet implemented");
return;
}
let conn = match scope.as_str() {
"user" => Connection::session().await,
_ => Connection::system().await,
@ -622,5 +634,42 @@ impl qobject::SystemdService {
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");
}
Err(e) => {
tracing::warn!(target: "nova_plugin", unit = %name, error = %e, "restart_unit busctl spawn failed");
}
}
}
}

View file

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

View file

@ -10,15 +10,17 @@ import NovaStats as NS
Column {
id: root
required property color accentColor
required property string machineName
required property string title
required property string marker
required property string systemState
required property int runningCount
required property int totalCount
required property var failedUnits
required property var runningUnits
property color accentColor
// SSH target string (for remote hosts/their containers); "" for local-side entries.
property string hostTarget: ""
property string machineName: ""
property string title: ""
property string marker: ""
property string systemState: "unknown"
property int runningCount: 0
property int totalCount: 0
property var failedUnits: []
property var runningUnits: []
property string errorKind: ""
property string errorReason: ""
property int lastSeen: 0
@ -169,6 +171,7 @@ Column {
description: modelData.description ?? ""
subState: modelData.subState ?? ""
scope: modelData.scope ?? "system"
hostTarget: root.hostTarget
machineName: root.machineName
accentColor: root.accentColor
}
@ -229,40 +232,47 @@ Column {
description: modelData.description ?? ""
subState: modelData.subState ?? ""
scope: modelData.scope ?? "system"
hostTarget: root.hostTarget
machineName: root.machineName
accentColor: root.accentColor
}
}
// 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 {
model: root.containers ?? []
delegate: Item {
id: _childWrap
required property var modelData
width: root.width
height: _child.height + 4
height: _childLoader.height + 4
SystemdMachineSection {
id: _child
Loader {
id: _childLoader
anchors.left: parent.left
anchors.leftMargin: 16
width: parent.width - 16
accentColor: root.accentColor
machineName: _childWrap.modelData.name
title: _childWrap.modelData.name
marker: _childWrap.modelData.marker ?? ""
systemState: _childWrap.modelData.systemState ?? "unknown"
runningCount: _childWrap.modelData.runningCount ?? 0
totalCount: _childWrap.modelData.totalCount ?? 0
failedUnits: _childWrap.modelData.failedUnits ?? []
runningUnits: _childWrap.modelData.runningUnits ?? []
errorKind: _childWrap.modelData.errorKind ?? ""
errorReason: _childWrap.modelData.errorReason ?? ""
lastSeen: _childWrap.modelData.lastSeen ?? 0
containers: _childWrap.modelData.containers ?? []
depth: root.depth + 1
onContentResized: root.contentResized()
source: "SystemdMachineSection.qml"
onLoaded: {
item.accentColor = root.accentColor;
item.hostTarget = root.hostTarget;
item.machineName = _childWrap.modelData.name;
item.title = _childWrap.modelData.name;
item.marker = _childWrap.modelData.marker ?? "";
item.systemState = _childWrap.modelData.systemState ?? "unknown";
item.runningCount = _childWrap.modelData.runningCount ?? 0;
item.totalCount = _childWrap.modelData.totalCount ?? 0;
item.failedUnits = _childWrap.modelData.failedUnits ?? [];
item.runningUnits = _childWrap.modelData.runningUnits ?? [];
item.errorKind = _childWrap.modelData.errorKind ?? "";
item.errorReason = _childWrap.modelData.errorReason ?? "";
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 subState
required property string scope
required property string hostTarget
required property string machineName
required property color accentColor
@ -96,7 +97,7 @@ Item {
}
TapHandler {
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) {
NS.SystemdService.restartUnit(name, scope ?? "system", machine ?? "");
function restartUnit(name, scope, host, machine) {
NS.SystemdService.restartUnit(name, scope ?? "system", host ?? "", machine ?? "");
}
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: 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/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 "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]