systemd: container/remote restart, permanent-error backoff, recursive section via loader (step 6)
This commit is contained in:
parent
5676b1ac62
commit
dfa3840d97
6 changed files with 118 additions and 56 deletions
|
|
@ -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");
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue