aboutsummaryrefslogtreecommitdiffstats
path: root/vm-manager/Virsh.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-14 12:32:19 +0200
committerDanilo M. <danix@danix.xyz>2026-09-14 12:32:19 +0200
commit315becb78eb48be186f2370ae6c5efd3e967c12d (patch)
tree66dad581d03ebbc4f49680099e955fe6c62612e2 /vm-manager/Virsh.qml
parent350b6358e29699721c636c18f8928acb68a05ade (diff)
downloadquickshell-315becb78eb48be186f2370ae6c5efd3e967c12d.tar.gz
quickshell-315becb78eb48be186f2370ae6c5efd3e967c12d.zip
feat(desktop): move vm-manager in as the vm module
Virsh.qml is unchanged; VmPanel's body becomes the page and the stat row becomes a Flow, because four stats with 150px bars do not fit a 600px drawer as one row. The page starts and stops Virsh.sampling, so the 2s poll runs only while something is looking at it; the lifecycle event stream still runs always, which is what keeps the tile's dots current.
Diffstat (limited to 'vm-manager/Virsh.qml')
-rw-r--r--vm-manager/Virsh.qml349
1 files changed, 0 insertions, 349 deletions
diff --git a/vm-manager/Virsh.qml b/vm-manager/Virsh.qml
deleted file mode 100644
index 14bd368..0000000
--- a/vm-manager/Virsh.qml
+++ /dev/null
@@ -1,349 +0,0 @@
-// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License version 2 as
-// published by the Free Software Foundation.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-pragma Singleton
-
-import Quickshell
-import Quickshell.Io
-import QtQuick
-
-// Everything that talks to libvirt. Shelling out to virsh rather than binding
-// libvirt: the commands are the same ones the old rofi script used, and this
-// adds no dependency.
-Singleton {
- id: root
-
- // name -> { state, cpu, memUsed, memTotal, fsUsed, fsTotal, ip, vcpus, agent, saved }
- property var vms: ({})
- property var names: []
- property var snapshots: ({}) // name -> [{ name, created, state, current }]
-
- // Polling only runs while the panel is open; the event stream always does.
- property bool sampling: false
-
- signal actionFailed(string vm, string action, string message)
-
- function _vm(name) {
- return vms[name] ?? { state: "unknown", cpu: -1, memUsed: -1, memTotal: -1,
- fsUsed: -1, fsTotal: -1, ip: "", vcpus: 0, agent: false,
- saved: false };
- }
-
- function _set(name, fields) {
- const next = Object.assign({}, vms);
- next[name] = Object.assign({}, _vm(name), fields);
- vms = next;
- }
-
- // --- discovery -------------------------------------------------------
-
- Process {
- id: listProc
- command: ["virsh", "list", "--all", "--name"]
- stdout: StdioCollector {
- onStreamFinished: {
- const found = text.trim().split("\n").map(s => s.trim()).filter(s => s.length);
- root.names = found;
- for (const n of found) if (!(n in root.vms)) root._set(n, {});
- root._pollSaved();
- root.refresh();
- }
- }
- }
-
- // --- state + stats ---------------------------------------------------
-
- // cpu.time is cumulative nanoseconds, so a percentage needs two samples.
- property var _lastCpu: ({})
-
- Process {
- id: statsProc
- command: ["virsh", "domstats", "--state", "--cpu-total", "--balloon", "--vcpu"]
- stdout: StdioCollector {
- onStreamFinished: {
- const now = Date.now();
- let vm = null;
- for (const line of text.split("\n")) {
- const dom = line.match(/^Domain: '(.+)'$/);
- if (dom) { vm = dom[1]; continue; }
- if (!vm) continue;
- const kv = line.trim().match(/^([\w.]+)=(.+)$/);
- if (!kv) continue;
- const [, key, val] = kv;
-
- if (key === "state.state") {
- root._set(vm, { state: root._stateName(parseInt(val)) });
- } else if (key === "vcpu.current") {
- root._set(vm, { vcpus: parseInt(val) });
- } else if (key === "cpu.time") {
- const ns = parseInt(val);
- const prev = root._lastCpu[vm];
- if (prev && now > prev.at) {
- const vcpus = root._vm(vm).vcpus || 1;
- const pct = (ns - prev.ns) / ((now - prev.at) * 1e6) / vcpus * 100;
- root._set(vm, { cpu: Math.max(0, Math.min(100, pct)) });
- }
- const c = Object.assign({}, root._lastCpu);
- c[vm] = { ns: ns, at: now };
- root._lastCpu = c;
- }
- }
- root._pollAgents();
- }
- }
- }
-
- function _stateName(n) {
- // libvirt VIR_DOMAIN_* enum
- return ({ 1: "running", 2: "blocked", 3: "paused", 4: "shutting down",
- 5: "shut off", 6: "crashed", 7: "suspended" })[n] ?? "unknown";
- }
-
- // --- guest agent -----------------------------------------------------
- //
- // Memory, filesystem usage and IP all come from qemu-guest-agent. It is
- // not up while the VM boots, and a VM may not have it at all, so every
- // one of these failing is ordinary: the rows show a dash rather than
- // falling back to a host-side number that means something different.
-
- property var _agentQueue: []
-
- function _pollAgents() {
- _agentQueue = names.filter(n => _vm(n).state === "running");
- _nextAgent();
- }
-
- function _nextAgent() {
- if (_agentQueue.length === 0) return;
- const vm = _agentQueue[0];
- _agentQueue = _agentQueue.slice(1);
- memProc.vm = vm;
- memProc.command = ["virsh", "dommemstat", vm];
- memProc.running = true;
- }
-
- Process {
- id: memProc
- property string vm: ""
- stdout: StdioCollector {
- onStreamFinished: {
- const get = k => {
- const m = text.match(new RegExp("^" + k + " (\\d+)$", "m"));
- return m ? parseInt(m[1]) * 1024 : -1;
- };
- const total = get("actual"), usable = get("usable");
- if (total > 0 && usable > 0) {
- memProc.vmSet({ memUsed: total - usable, memTotal: total, agent: true });
- } else {
- memProc.vmSet({ memUsed: -1, memTotal: total, agent: false });
- }
- fsProc.vm = memProc.vm;
- fsProc.command = ["virsh", "qemu-agent-command", memProc.vm,
- '{"execute":"guest-get-fsinfo"}'];
- fsProc.running = true;
- }
- }
- function vmSet(f) { root._set(vm, f); }
- onExited: code => { if (code !== 0) root._set(vm, { agent: false, memUsed: -1 }); }
- }
-
- Process {
- id: fsProc
- property string vm: ""
- stdout: StdioCollector {
- onStreamFinished: {
- try {
- const fs = JSON.parse(text).return;
- // The root filesystem is the one worth showing; the ESP is noise.
- const r = fs.find(f => f.mountpoint === "/") ?? fs[0];
- if (r && r["total-bytes"] > 0)
- root._set(fsProc.vm, { fsUsed: r["used-bytes"], fsTotal: r["total-bytes"] });
- } catch (e) {
- root._set(fsProc.vm, { fsUsed: -1, fsTotal: -1 });
- }
- ipProc.vm = fsProc.vm;
- ipProc.command = ["virsh", "domifaddr", fsProc.vm, "--source", "agent"];
- ipProc.running = true;
- }
- }
- onExited: code => { if (code !== 0) root._set(vm, { fsUsed: -1, fsTotal: -1 }); }
- }
-
- Process {
- id: ipProc
- property string vm: ""
- stdout: StdioCollector {
- onStreamFinished: {
- // Skip loopback and link-local; the first real v4 address wins.
- const m = text.match(/\s(\d+\.\d+\.\d+\.\d+)\/\d+/g) ?? [];
- const ip = m.map(s => s.trim().split("/")[0])
- .find(a => !a.startsWith("127.")) ?? "";
- root._set(ipProc.vm, { ip: ip });
- root._nextAgent();
- }
- }
- onExited: code => { if (code !== 0) { root._set(vm, { ip: "" }); root._nextAgent(); } }
- }
-
- // --- managed save ----------------------------------------------------
- //
- // A shut-off VM can still carry a saved memory image, and `virsh start`
- // then restores it rather than booting. When that image cannot be
- // restored the start fails every time with a QEMU migrate-incoming
- // error, and nothing in the panel said why. `domstats` does not report
- // it: the shut-off reason reads "failed", from the failed start, not
- // from the save. `dominfo` is the only cheap source, so it is polled per
- // VM the way the agent rows are.
-
- property var _savedQueue: []
-
- function _pollSaved() {
- _savedQueue = names.slice();
- _nextSaved();
- }
-
- function _nextSaved() {
- if (_savedQueue.length === 0) return;
- const vm = _savedQueue[0];
- _savedQueue = _savedQueue.slice(1);
- savedProc.vm = vm;
- savedProc.command = ["virsh", "dominfo", vm];
- savedProc.running = true;
- }
-
- Process {
- id: savedProc
- property string vm: ""
- stdout: StdioCollector {
- onStreamFinished: {
- root._set(savedProc.vm, { saved: /^Managed save:\s+yes$/m.test(text) });
- root._nextSaved();
- }
- }
- onExited: code => { if (code !== 0) { root._set(vm, { saved: false }); root._nextSaved(); } }
- }
-
- // --- snapshots -------------------------------------------------------
-
- Process {
- id: snapProc
- property string vm: ""
- stdout: StdioCollector {
- onStreamFinished: {
- const rows = [];
- for (const line of text.split("\n").slice(2)) {
- // Name, then creation date and time, then state.
- const m = line.trim().match(/^(\S+)\s+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})[^\s]*\s*\S*\s+(\S+)/);
- if (m) rows.push({ name: m[1], created: m[2], state: m[3] });
- }
- const next = Object.assign({}, root.snapshots);
- next[snapProc.vm] = rows;
- root.snapshots = next;
- }
- }
- }
-
- function loadSnapshots(vm) {
- snapProc.vm = vm;
- snapProc.command = ["virsh", "snapshot-list", vm];
- snapProc.running = true;
- }
-
- // --- actions ---------------------------------------------------------
-
- readonly property var _cmds: ({
- start: v => ["virsh", "start", v],
- shutdown: v => ["virsh", "shutdown", "--mode", "acpi", "--domain", v],
- reboot: v => ["virsh", "reboot", v],
- reset: v => ["virsh", "reset", v],
- destroy: v => ["virsh", "destroy", v],
- suspend: v => ["virsh", "suspend", v],
- resume: v => ["virsh", "resume", v],
- discardsave: v => ["virsh", "managedsave-remove", v],
- })
-
- function act(vm, action) {
- actProc.vm = vm; actProc.action = action;
- actProc.command = _cmds[action](vm);
- actProc.running = true;
- }
-
- function snapshotCreate(vm) {
- const ts = Qt.formatDateTime(new Date(), "ddMMyyyy_HHmmss");
- actProc.vm = vm; actProc.action = "snapshot";
- actProc.command = ["virsh", "snapshot-create-as", "--domain", vm,
- "--name", `${vm}_${ts}`];
- actProc.running = true;
- }
-
- function snapshotRevert(vm, snap) {
- actProc.vm = vm; actProc.action = "revert";
- actProc.command = ["virsh", "snapshot-revert", vm, snap];
- actProc.running = true;
- }
-
- function snapshotDelete(vm, snap) {
- actProc.vm = vm; actProc.action = "snapshot delete";
- actProc.command = ["virsh", "snapshot-delete", vm, snap];
- actProc.running = true;
- }
-
- // Undefine with --remove-all-storage erases the disk image. The panel
- // requires the VM's name typed before it will call this.
- function deleteVm(vm) {
- actProc.vm = vm; actProc.action = "delete";
- actProc.command = ["sh", "-c",
- `virsh destroy ${JSON.stringify(vm)} 2>/dev/null; ` +
- `virsh undefine ${JSON.stringify(vm)} --remove-all-storage`];
- actProc.running = true;
- }
-
- Process {
- id: actProc
- property string vm: ""
- property string action: ""
- stderr: StdioCollector { id: actErr }
- onExited: code => {
- if (code !== 0)
- root.actionFailed(actProc.vm, actProc.action,
- actErr.text.trim() || `exited ${code}`);
- root.refreshList();
- if (root.snapshots[actProc.vm]) root.loadSnapshots(actProc.vm);
- }
- }
-
- // --- refresh ---------------------------------------------------------
-
- function refresh() { statsProc.running = true; }
- function refreshList() { listProc.running = true; }
-
- Timer {
- interval: 2000
- running: root.sampling
- repeat: true
- onTriggered: root.refresh()
- }
-
- // libvirt pushes lifecycle changes, so a VM started from virt-manager or
- // the CLI updates the panel too. This runs whether or not it is open,
- // which is what makes the panel correct the moment it is shown.
- Process {
- running: true
- command: ["virsh", "event", "--all", "--loop"]
- stdout: SplitParser {
- onRead: line => {
- if (/event '(lifecycle|agent-lifecycle)'/.test(line)) root.refreshList();
- }
- }
- }
-
- Component.onCompleted: refreshList()
-}