diff --git a/plugin/src/systemd_service.rs b/plugin/src/systemd_service.rs index 4d0b754..4b55cb0 100644 --- a/plugin/src/systemd_service.rs +++ b/plugin/src/systemd_service.rs @@ -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,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 scope = scope.to_string(); + let host = host.to_string(); let machine = machine.to_string(); let _ = self; - 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; + + // Local + system or user scope: native zbus, supports user manager too. + if host.is_empty() && machine.is_empty() { + rt().block_on(async move { + 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 = 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() { - "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"); + Err(e) => { + tracing::warn!(target: "nova_plugin", unit = %name, error = %e, "restart_unit busctl spawn failed"); } - }); + } } } diff --git a/shell/applets/SystemdApplet.qml b/shell/applets/SystemdApplet.qml index 795ab09..756cba1 100644 --- a/shell/applets/SystemdApplet.qml +++ b/shell/applets/SystemdApplet.qml @@ -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" diff --git a/shell/applets/SystemdMachineSection.qml b/shell/applets/SystemdMachineSection.qml index e725cb3..e10428f 100644 --- a/shell/applets/SystemdMachineSection.qml +++ b/shell/applets/SystemdMachineSection.qml @@ -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); + } } } } diff --git a/shell/applets/SystemdUnitRow.qml b/shell/applets/SystemdUnitRow.qml index 4c4f2a0..3522791 100644 --- a/shell/applets/SystemdUnitRow.qml +++ b/shell/applets/SystemdUnitRow.qml @@ -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) } } } diff --git a/shell/services/SystemdService.qml b/shell/services/SystemdService.qml index 3a82048..8d2e4e2 100644 --- a/shell/services/SystemdService.qml +++ b/shell/services/SystemdService.qml @@ -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() { diff --git a/test/qmllint-baseline.txt b/test/qmllint-baseline.txt index aab93d3..6717fde 100644 --- a/test/qmllint-baseline.txt +++ b/test/qmllint-baseline.txt @@ -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]