diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-14 12:32:19 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-14 12:32:19 +0200 |
| commit | 315becb78eb48be186f2370ae6c5efd3e967c12d (patch) | |
| tree | 66dad581d03ebbc4f49680099e955fe6c62612e2 /vm-manager/Virsh.qml | |
| parent | 350b6358e29699721c636c18f8928acb68a05ade (diff) | |
| download | quickshell-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.qml | 349 |
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() -} |
