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 页面里:

另外两台(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 设备丢弃——于是:

浏览器 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 的默认设置 → 决定用谁打开 + "打开方式"列表
用途要不要写
纯启动器(本例 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 是显卡,virgl 是这块显卡的 3D 特性,SPICE 是把画面(含 3D 渲染结果)传出来的管道。

排查技巧沉淀

  1. 先看最直接的数据源:前端渲染异常时,第一反应应该是 dump 出它消费的原始数据(本例是 virsh dumpxml),而不是从守护进程开始向上排查;
  2. busctl 直接调 D-Bus 方法可以一把绕过三层(前端/bridge/代理)验证服务端;
  3. 模拟 cockpit-bridge 的 dbus-json3 通道:帧协议是 <十进制长度>\n<channel>\n<payload>,channel 为空行即控制消息,客户端必须先发 {"command":"init","version":1,"host":...} 才能开通道,调用格式 {"id":..,"call":[path,iface,method,args]};
  4. 前端 bundle 解包 grep:gunzip -c index.js.gz 后按函数特征(getElementsByTagName("graphics") 等)定位解析逻辑,比读源码仓库更快对应到实际部署版本。

相关阅读