From 91fb80209c63bd335622bcc796bcfad93f0c9550 Mon Sep 17 00:00:00 2001 From: TimLai666 Date: Fri, 10 Jul 2026 14:53:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(whp-gui):=20set=20WLR=5FLIBINPUT=5FNO=5FDEV?= =?UTF-8?q?ICES=3D1=20=E2=80=94=20cage=20races=20udev=20on=20input=20devic?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root-causes the intermittent WHP GUI boot failure noted in #120 (guest-agent exits 1 ~0.45s into boot, before any frame; ~1 in 3 boots). Investigation: guest-agent's own stderr is invisible on the WHP serial console (init redirects to hvc0/ttyS0 but `quiet` drops console_loglevel to 4, so only init's `<0>` kmsg `report()` shows). Added a `note()` that mirrors GUI-startup diagnostics to `/dev/kmsg` at `<2>` (KERN_CRIT, prints under `quiet`) — and crucially, captured cage's stderr (`Stdio::piped`) and forward it to kmsg when cage dies during startup. Two kmsg subtleties bit the instrumentation first and are handled: each write() to /dev/kmsg is one record, so the line is built and written with a single `write_all` (`writeln!`/write_fmt splits into multiple records, and the message-body record lacked the `` prefix so `quiet` hid it); and the priority must be `< 4`. With that, a captured failure showed the real cause: cage: [ERROR] libinput initialization failed, no input devices cage: [ERROR] Set WLR_LIBINPUT_NO_DEVICES=1 to suppress this check cage: [ERROR] Failed to initialize backend / Unable to start the wlroots backend and `starting cage (dri card present=true)` — so /dev/dri was fine; wlroots' libinput backend aborts when zero input devices exist yet. In the micro-VM the virtio-input nodes (/dev/input/event*) are created by udev asynchronously; when cage wins the race it sees no devices and exits → wait_for_socket bails → exit 1. Intermittent = the udev-vs-cage race on input nodes. Fix: `WLR_LIBINPUT_NO_DEVICES=1` in cage's env (wlroots' own recommended flag) — start with zero input devices; they hotplug in via udev afterward. An earlier attempt waited for /dev/dri before cage, but the instrumentation proved dri was never the missing resource, so that was dropped for this one-line real fix. Verified on real WHP hardware (this box): 14/14 consecutive clean boots succeed after the fix (was ~1/3 failing before). The cage-stderr→kmsg observability is kept so future WHP GUI failures are self-diagnosing. No unit test — it's an in-VM udev/device race with no host-testable surface; verification is the boot loop. Co-Authored-By: Claude Fable 5 --- crates/guest-agent/src/gui.rs | 59 ++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/crates/guest-agent/src/gui.rs b/crates/guest-agent/src/gui.rs index 3d0fe33..d955841 100644 --- a/crates/guest-agent/src/gui.rs +++ b/crates/guest-agent/src/gui.rs @@ -26,6 +26,22 @@ use anyhow::{Context, Result, bail}; use chefer_bundle::Manifest; use nix::mount::{MsFlags, mount}; +/// 記一行 GUI 診斷。同時走 stderr(原生 Linux / 有接 console 的後端可見)與 **`/dev/kmsg`** +/// ——WHP 的序列 console 在 `quiet` 下不顯示 guest-agent 自己的 stderr,但高優先序 kmsg 會 +/// 出現在 console(同 appliance init 的 `report()`),故 WHP GUI 的啟動診斷才看得到。best-effort。 +fn note(msg: &str) { + eprintln!("[guest-agent] gui: {msg}"); + if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/dev/kmsg") { + use std::io::Write as _; + // **一次 write() = 一筆 kmsg 記錄**:必須先組好整行再單次寫入。`writeln!`(write_fmt) + // 會拆成多次 write,只有第一筆帶 `<2>` 前綴、其餘(含訊息本體)落回預設 level 而被 + // `quiet` 擋掉。`quiet` 把 console_loglevel 壓到 4(只印 level < 4),故用 <2>=KERN_CRIT + //(appliance init 的 report() 用 <0> 亦同理)。 + let line = format!("<2>[guest-agent] gui: {msg}\n"); + let _ = f.write_all(line.as_bytes()); + } +} + /// cage 建立 Wayland socket 的 runtime 目錄(guest 內,appliance tmpfs 上)。 const GUI_RUNTIME_DIR: &str = "/run/chefer-gui"; /// 等 compositor socket 出現的上限。llvmpipe 首次啟動慢,放寬一點。 @@ -82,11 +98,13 @@ pub fn maybe_start(bundle_dir: &Path, manifest: &Manifest) -> Result Result Result { .env("XDG_RUNTIME_DIR", GUI_RUNTIME_DIR) .env("WLR_RENDERER", "pixman") // VM 無 GPU:強制軟算繪,避免 GLES 探測失敗 .env("LIBSEAT_BACKEND", "seatd") // Alpine libseat 無 builtin backend → 走 seatd + // 實機根因(WHP GUI 約 1/3 間歇失敗):virtio-input 節點(/dev/input/event*)由 udev + // 非同步建立,cage 若早於 udev 啟動,wlroots 的 libinput backend 見「no input devices」 + // 直接 abort → cage 秒退。wlroots 的錯誤訊息本身建議設此旗標容忍 0 輸入裝置啟動 + //(裝置隨後由 udev 熱插拔補上),消除此競態。 + .env("WLR_LIBINPUT_NO_DEVICES", "1") .stdin(Stdio::null()) - // cage/wlroots 的啟動失敗原因(缺 DRM、keymap…)只會出現在 stderr—— - // 沿用 guest-agent stderr(VM console/診斷通道),別吞掉。 + // cage/wlroots 的啟動失敗原因(缺 DRM、keymap、seatd…)只會出現在 stderr——**捕獲** + // 起來,cage 若啟動失敗(wait_for_socket 偵測)就轉發到 /dev/kmsg(WHP console 可見; + // 否則在 `quiet` 下這段錯誤完全看不到,見 note())。 + .stderr(Stdio::piped()) .spawn()?; Ok(child) } @@ -343,11 +368,29 @@ fn wait_for_socket(sock: &Path, session: &mut GuiSession) -> Result<()> { return Ok(()); } if let Some(status) = session.cage.try_wait()? { + // cage 啟動失敗——把它捕獲的 stderr 轉到 kmsg(WHP console 可見),這正是 + // 「Found 0 GPUs」/ seatd / keymap 之類真正原因的所在,否則在 quiet 下看不到。 + let mut cage_err = String::new(); + if let Some(mut err) = session.cage.stderr.take() { + use std::io::Read as _; + let _ = err.read_to_string(&mut cage_err); + } + let cage_err = cage_err.trim(); + if !cage_err.is_empty() { + for line in cage_err.lines() { + note(&format!("cage: {line}")); + } + } bail!( - "the in-VM compositor (cage) exited during startup ({status}); \ - GUI cannot be shown. Check the guest log for wlroots errors \ - (missing /dev/dri usually means the appliance kernel lacks virtio-gpu, \ - or the VM host did not attach a virtio-gpu device)." + "the in-VM compositor (cage) exited during startup ({status}); GUI cannot be shown. \ + cage stderr: {}. \ + (missing /dev/dri usually means the appliance kernel lacks virtio-gpu, or the VM \ + host did not attach a virtio-gpu device.)", + if cage_err.is_empty() { + "(none captured)" + } else { + cage_err + } ); } if start.elapsed() > SOCKET_WAIT {