diff options
Diffstat (limited to 'desktop/modules/vm/Docker.qml')
| -rw-r--r-- | desktop/modules/vm/Docker.qml | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/desktop/modules/vm/Docker.qml b/desktop/modules/vm/Docker.qml new file mode 100644 index 0000000..10199b2 --- /dev/null +++ b/desktop/modules/vm/Docker.qml @@ -0,0 +1,149 @@ +// 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 + +// The docker half of the vm page, shaped like Virsh: the docker CLI, an +// event stream that always runs, and a stats poll only while the page is up. +// Containers are grouped by compose project, because one compose stack is a +// dozen containers that start and stop together; a container outside any +// project is a group of one. +Singleton { + id: root + + // project -> [{ id, name, state, status, image }], names sorted + property var groups: ({}) + property var projects: [] + property var stats: ({}) // short id -> { cpu, mem, memTotal } + + property bool sampling: false + + signal actionFailed(string target, string action, string message) + + // --- discovery ------------------------------------------------------- + + Process { + id: listProc + command: ["docker", "ps", "-a", "--format", + '{{.ID}}\t{{.Names}}\t{{.State}}\t{{.Status}}\t{{.Label "com.docker.compose.project"}}\t{{.Image}}'] + stdout: StdioCollector { + onStreamFinished: { + const g = {}; + for (const line of text.split("\n")) { + const f = line.split("\t"); + if (f.length < 6) continue; + const p = f[4] || f[1]; + (g[p] = g[p] ?? []).push({ id: f[0], name: f[1], state: f[2], + status: f[3], image: f[5] }); + } + for (const p in g) g[p].sort((a, b) => a.name.localeCompare(b.name)); + root.groups = g; + root.projects = Object.keys(g).sort(); + } + } + // No daemon, or no permission on the socket: the section is empty. + onExited: code => { if (code !== 0) { root.groups = {}; root.projects = []; } } + } + + // --- stats ----------------------------------------------------------- + // + // `docker stats --no-stream` samples for about 2s before printing, which + // is the same cadence as the timer; a tick that lands while it is still + // running is a no-op. CPU is docker's own figure, 100% per core. + + function _bytes(s) { + const m = s.match(/([\d.]+)\s*([kKMGT]?i?B)/); + if (!m) return -1; + const k = { B: 1, kB: 1e3, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12, + KiB: 1024, MiB: 1024 ** 2, GiB: 1024 ** 3, TiB: 1024 ** 4 }[m[2]] ?? 1; + return parseFloat(m[1]) * k; + } + + Process { + id: statsProc + command: ["docker", "stats", "--no-stream", "--format", + "{{.ID}}\t{{.CPUPerc}}\t{{.MemUsage}}"] + stdout: StdioCollector { + onStreamFinished: { + const s = {}; + for (const line of text.split("\n")) { + const f = line.split("\t"); + if (f.length < 3) continue; + const [used, total] = f[2].split("/"); + s[f[0]] = { cpu: parseFloat(f[1]), mem: root._bytes(used), + memTotal: root._bytes(total ?? "") }; + } + root.stats = s; + } + } + } + + // --- actions --------------------------------------------------------- + + // ids: the containers to act on; target names them in a failure notice. + function act(ids, target, action) { + actProc.target = target; actProc.action = action; + actProc.command = ["docker", action].concat(ids); + actProc.running = true; + } + + Process { + id: actProc + property string target: "" + property string action: "" + stderr: StdioCollector { id: actErr } + onExited: code => { + if (code !== 0) + root.actionFailed(actProc.target, actProc.action, + actErr.text.trim() || `exited ${code}`); + root.refreshList(); + } + } + + // --- refresh --------------------------------------------------------- + + function refreshList() { listProc.running = true; } + + Timer { + interval: 2000 + running: root.sampling + repeat: true + triggeredOnStart: true + onTriggered: statsProc.running = true + } + + // Only lifecycle events: health checks fire exec_* events every few + // seconds per container, which would turn this into a poll. The stream + // dies with the daemon, so it is retried rather than left dead. + Process { + id: events + running: true + command: ["docker", "events", "--filter", "type=container", + "--filter", "event=create", "--filter", "event=start", + "--filter", "event=die", "--filter", "event=pause", + "--filter", "event=unpause", "--filter", "event=destroy", + "--filter", "event=health_status", + "--format", "{{.Action}}"] + stdout: SplitParser { onRead: root.refreshList() } + onRunningChanged: if (running) root.refreshList() + onExited: retry.start() + } + + Timer { + id: retry + interval: 10000 + onTriggered: events.running = true + } +} |
