cockpit-machines 只剩串口 console?——SPICE unix socket 监听踩坑记
十月 09, 2026 [troubleshooting, virtualization] #qemu #debug现象
宿主机上三台 virsh 管理的虚拟机(qemu:///system 下),其中日常开发机 ubuntu.code(16 vCPU / 32G,virtiofs 共享目录 + virgl 3D 加速)在 cockpit 的 Virtual machines 页面里:
- 详情页 Console 卡片只剩 Serial Console(串口);
- 图形 console 消失;
- 连 SPICE 模式下应有的 Launch Remote Viewer 按钮(下载
.vv文件)也没有了。
另外两台(106、win11)一切正常。
排查过程
第一步(弯路):自底向上查基础设施
一开始的思路是"数据链路上某层断了",于是从底往上逐层验证:
# libvirtd 与 libvirt-dbus 服务状态
systemctl status libvirtd # active,monolithic legacy daemon
busctl list | grep libvirt # 系统总线上 org.libvirt 存在(libvirt-dbus --system)
# 直接实测 D-Bus 方法(绕过 cockpit 前端)
busctl call org.libvirt /org/libvirt/QEMU org.libvirt.Connect ListDomains u 0
# → 正常返回 3 个 domain 对象路径
busctl call org.libvirt /org/libvirt/QEMU/domain/<uuid> \
org.freedesktop.DBus.Properties GetAll s org.libvirt.Domain
# → 正常返回 Name/Active/Id/UUID...
busctl call org.libvirt /org/libvirt/QEMU org.libvirt.Connect ListStoragePools u 0
busctl call org.libvirt <pool-path> org.freedesktop.DBus.Properties GetAll s org.libvirt.StoragePool
# → pools/networks 全部正常
D-Bus 层全绿。权限层也排除了:org.libvirt.conf 允许 libvirt 组向 org.libvirt 发消息,用户在该组;polkit 规则放行 libvirtdbus。
甚至写了个 Python 脚本模拟浏览器驱动 cockpit-bridge:用正确的帧协议(<长度>\n<channel>\n<payload>)打开 dbus-json3 通道调 ListDomains——bridge 端到端也正常返回。
这段弯路的教训:从底往上排查前,先确认最直接的数据源。前端渲染 VM 详情的原料就是
DomainGetXMLDesc的 XML,而这个从第一步就该直接 dump 出来看。
第二步:对比三台 VM 的 XML
virsh -c qemu:///system dumpxml ubuntu.code > u.xml
virsh -c qemu:///system dumpxml 106 > 1.xml
virsh -c qemu:///system dumpxml win11 > w.xml
diff 之后,ubuntu.code 独有的 graphics 定义浮出水面:
<!-- ubuntu.code:unix socket 监听(virgl 配套) -->
<graphics type='spice'>
<listen type='socket' socket='/var/lib/libvirt/qemu/domain-7-ubuntu.code/spice.sock'/>
<image compression='off'/>
<gl enable='yes' rendernode='/dev/dri/renderD128'/>
</graphics>
<!-- 106 / win11:TCP 监听,属性齐全 -->
<graphics type='spice' autoport='yes' listen='0.0.0.0'>
<listen type='address' address='0.0.0.0'/>
</graphics>
关键差异:ubuntu.code 的 <graphics> 元素属性上没有 autoport / listen / port / tlsPort——监听信息全在 <listen type='socket'> 子元素里。
第三步:解包前端代码找解析逻辑
cockpit-machines 是打包压缩的 React 应用,直接 grep 解包产物:
gunzip -c /usr/share/cockpit/machines/index.js.gz > index.js
grep -o 'function wie(e){.\{0,900\}' index.js
找到 parseDumpxmlForConsoles()(minified 后叫 wie):
function parseDumpxmlForConsoles(e) {
let t = [], o = e.getElementsByTagName("graphics");
if (o) for (let a = 0; a < o.length; a++) {
let n = o[a], i = {
type: n.getAttribute("type"), // "spice"
port: n.getAttribute("port"), // socket 监听 → null
tlsPort: n.getAttribute("tlsPort"), // null
address: n.getAttribute("listen"), // null(listen 是子元素)
autoport: n.getAttribute("autoport"), // null
};
i.type && (i.autoport || i.address && (i.port || i.tlsPort))
? t.push(i) // 属性齐全才收录
: console.warn("parseDumpxmlForConsoles(): mandatory properties are missing in dumpxml, found: " + JSON.stringify(i));
// socket 监听 → 走 warn 分支,整个 graphics 被丢弃!
}
// ...console type='pty'(串口)无条件收录
}
而 Console 卡片的类型决策同样消费这份解析结果:
displays.find(c => c.type == "spice") ? "DesktopViewer" // Launch Remote Viewer 按钮
: (hasSerial ? "SerialConsole" : null)
根因
virtio-gpu virgl 3D 加速要求 SPICE 使用 unix socket 监听(<gl enable='yes'> 与 TCP listen 互斥,本地渲染语义),socket 监听时 <graphics> 元素属性上没有任何 port/listen/autoport;cockpit-machines 332 的 parseDumpxmlForConsoles() 只认这些属性,判定 "mandatory properties are missing" 后把整个 graphics 设备丢弃——于是:
- 图形 console 消失;
displays里没有 spice 项 →"DesktopViewer"类型不成立 → Launch Remote Viewer 按钮一起消失(两者共用同一份解析结果);- 落到 fallback → 只渲染 SerialConsole。
浏览器 F12 的 Console 里能看到那条 warn 作为佐证。另外还有一层设计原因:.vv 文件需要 host/port 的 TCP 地址,remote-viewer 无法连接远程 unix socket,所以上游对 socket 监听就是按"没有图形设备"处理。
方案取舍
virgl 3D 加速与 cockpit 网页端图形 console 不可兼得:
| 方案 | cockpit 网页端 | virgl | 代价 |
|---|---|---|---|
| SPICE + TCP(去 gl) | 恢复 Launch viewer | ❌ | 关机重启生效 |
| VNC + TCP | 内嵌 noVNC 直接看(唯一支持内嵌) | ❌ | 丢音频/剪贴板/USB 重定向 |
| 保持 SPICE socket + virgl(现状) | 只剩串口 | ✅ | 图形入口改走 virt-viewer |
最终选择保持现状,图形入口用 virt-viewer 直连(本地 unix socket,virgl 照常生效):
virt-viewer -c qemu:///system ubuntu.code
顺手加了个 zsh abbr(dotfiles/.my_abbr.d/vm):
abbr add --force --quiet view-vm='virt-viewer -c qemu:///system'
# view-vm ubuntu.code
入口优化:desktop 启动器与 GNOME 集成的三个坑
图形入口从 cockpit 网页搬到 virt-viewer 后,为了顺手做了两层入口:终端里的 zsh abbr(上文 view-vm),以及桌面环境的启动器 ~/.local/share/applications/local-vm.desktop:
[Desktop Entry]
Name=local-vm
Comment=Connect to local VM via SPICE
Exec=virt-viewer -c qemu:///system ubuntu.code
Type=Application
Terminal=false
Icon=virt-manager
Categories=System;X-Virtualization;
配这个文件的过程里踩了三个坑,都和"改完为什么不生效"有关。
坑 1:update-desktop-database 的报错是烟雾弹
改完文件跑了 update-desktop-database,输出:
The databases in [/home/peter/.local/share/flatpak/exports/share/applications,
/var/lib/flatpak/exports/share/applications, /usr/local/share/applications,
/usr/share/applications, ...] could not be updated.
看起来像出错了,实际是不带参数时它会尝试更新所有 XDG 数据目录(含 /usr/share/applications 等系统目录),普通用户没有写权限,列出来的就是这些;注意列表里没有 ~/.local/share/applications——用户目录是更新成功的。
更重要的是:这个命令维护的是 MIME 文件关联缓存,而 GNOME Shell 启动应用根本不查它。对一个没有 MimeType= 字段的启动器,跑了也是空转。
坑 2:真正决定 Exec 的是 GNOME Shell 的内存缓存
在 apps 里点图标,启动的还是旧的 Exec——文件明明已经改了、也没有同名 desktop 文件覆盖。原因:GNOME Shell 的应用列表(Activities 搜索、应用网格)来自 ShellAppSystem 进程内存里的缓存,靠 inotify 增量感知文件变化;很多编辑器"写临时文件 + rename"的原子替换偶尔不会触发重扫。
三个"缓存"各管一摊,别再跑错命令:
| 缓存 | 管什么 | 启动器场景相关吗 |
|---|---|---|
update-desktop-database / mimeinfo.cache | 文件类型 → 程序关联(打开 .pdf 用谁) | ❌ 启动应用不查它 |
GTK 图标缓存 (gtk-update-icon-cache) | 图标渲染 | ❌ 只有图标不刷新时相关 |
| GNOME Shell 应用列表 (ShellAppSystem) | 搜索/网格里显示什么、Exec 是什么 | ✅ 就是它没刷新 |
处理方法按代价排序:
# 1. 改 mtime 踢一脚 inotify
touch ~/.local/share/applications/local-vm.desktop
# 2. 立刻想用:直接读文件启动,绕过 Shell 缓存(也是测试 desktop 文件的最快方式)
gio launch ~/.local/share/applications/local-vm.desktop
# 3. 仍不生效 → 注销重登
# Wayland 会话没有 Alt+F2 敲 r 重启 Shell 的能力,注销是唯一彻底重置方式
坑 3:Categories 的未注册值
desktop-file-validate 抓出的真问题:
error: value "System;Virtualization;" for key "Categories" ... contains an
unregistered value "Virtualization"; values extending the format should start with "X-"
桌面菜单规范维护着注册分类表,Virtualization 不在其中,自定义值必须 X- 前缀:改成 Categories=System;X-Virtualization; 后校验通过,gio launch 实测拉起 virt-viewer 进程。
附:MimeType= 字段写不写?
MimeType= 声明"这个应用能打开哪些类型的文件/URL",是 desktop 文件接入文件关联系统的钩子:
双击 foo.pdf → 探测 MIME 类型 → 查 mimeinfo.cache(倒排索引)
→ 结合 ~/.config/mimeapps.list 的默认设置 → 决定用谁打开 + "打开方式"列表
- desktop 文件的
MimeType=是能力声明("我能处理这些"); mimeapps.list决定默认用谁;mimeinfo.cache只是加速反查的缓存(所以没有MimeType=的应用不会进这个索引)。
| 用途 | 要不要写 |
|---|---|
| 纯启动器(本例 local-vm) | ❌ 不写,写了反而可能抢关联 |
| 文件处理器(编辑器、播放器) | ✅ 必须写,否则不进"打开方式" |
| 协议处理器(浏览器、IDE) | ✅ 用 x-scheme-handler/… |
local-vm.desktop 的语义是"连接到固定一台 VM 的动作",没有任何"打开某类文件"的含义,不写是正确姿势。哪天想让"双击 .vv 文件就用 virt-viewer 打开",才需要给 virt-viewer 的 desktop 文件加 MimeType=application/x-virt-viewer; 并跑一次 update-desktop-database ~/.local/share/applications。
概念辨析:virgl、virtio-gpu、SPICE、VNC
- virtio-gpu 是一种半虚拟化显卡设备(
<video><model type='virtio'/>),有 2D(纯帧缓冲)和 3D 两种模式; - virgl 是 virtio-gpu 的 3D 加速机制(宿主侧 virglrenderer 把 guest 的 OpenGL 命令转译给真实 GPU),开启需要
video/model的accel3d='yes'与 SPICE 的gl enable='yes'配合; - SPICE / VNC 是图形传输协议(一台 VM 二选一)。SPICE 功能强(音频、剪贴板、USB 重定向、virgl 配套),但查看器基本只有 virt-viewer/remote-viewer;VNC 生态最广、有浏览器端 noVNC,但没有任何加速与外设透传。
一句话:virtio-gpu 是显卡,virgl 是这块显卡的 3D 特性,SPICE 是把画面(含 3D 渲染结果)传出来的管道。
排查技巧沉淀
- 先看最直接的数据源:前端渲染异常时,第一反应应该是 dump 出它消费的原始数据(本例是
virsh dumpxml),而不是从守护进程开始向上排查; - busctl 直接调 D-Bus 方法可以一把绕过三层(前端/bridge/代理)验证服务端;
- 模拟 cockpit-bridge 的 dbus-json3 通道:帧协议是
<十进制长度>\n<channel>\n<payload>,channel 为空行即控制消息,客户端必须先发{"command":"init","version":1,"host":...}才能开通道,调用格式{"id":..,"call":[path,iface,method,args]}; - 前端 bundle 解包 grep:
gunzip -c index.js.gz后按函数特征(getElementsByTagName("graphics")等)定位解析逻辑,比读源码仓库更快对应到实际部署版本。
相关阅读
- cockpit管理qemu虚拟机(virt-viewer 登录、SPICE 远程连接与 pybridge 断连 bug)
- virtiofs ENFILE 排查(同一台 VM 的另一段故事)