第四章的最後一天。前面幾天講的問題有一個共通點:它們只在 ARM64 的 CI runner 上發生。
而只在 CI 上發生的問題,是最難修的一種——每改一行就要推一次、等三分鐘、看 log 猜。今天講怎麼把那台機器搬到自己桌上。
在 CI 上除錯的迴圈大概是這樣:
一次假設驗證要八分鐘,而且你不能下中斷點、不能互動、不能「再試一次看看」。
如果那個問題需要二十次嘗試才能定位,這就是三個小時。而同樣的二十次嘗試在本機是二十分鐘。
Day 13 那個「注音吃掉按鍵」的問題,就是這樣解出來的——我在本機切換佈局、重跑、再切換,前後不到十分鐘。如果只有 CI,我大概會把它歸類成「ARM64 的輸入問題」然後加一個 retry 了事。
好消息是:MacBook 是 ARM64,Windows 11 有 ARM64 版本,所以這是原生虛擬化,不是模擬——速度是可用的。
我用 QEMU + HVF(macOS 的硬體加速框架):
qemu-system-aarch64 \
-M virt,highmem=on -accel hvf -cpu host -smp 4 -m 8192 \
-drive if=pflash,format=raw,readonly=on,file=edk2-aarch64-code.fd \
-drive if=pflash,format=raw,file=nvram.fd \
-device nvme,drive=hd,serial=win11 \
-drive if=none,id=hd,file=disk.qcow2,format=qcow2 \
-device virtio-net-pci,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-qmp unix:qmp.sock,server=on,wait=off
幾個關鍵:-M virt 是通用的 ARM 虛擬機器板,UEFI 韌體用 edk2,硬碟走 NVMe(比模擬 SATA 快很多),hostfwd 把 guest 的 22 埠映到 host 的 2222 讓你之後能 SSH。
安裝要全自動。 我用 autounattend.xml——就是 Day 2 提過那個設定自動登入的檔案。手動點完 Windows 安裝流程一次不難,但你會需要重灌好幾次,而且每次都得記得那十幾個選項。
usb-net 的驅動第一次開機後完全沒有網路。我用的是 usb-net(模擬 RNDIS),而 Windows 11 把 RNDIS 的內建驅動移除了。
換成 virtio-net-pci 之後還是不通,因為 Windows 沒有內建 virtio 驅動。要另外裝——而且一般的 virtio-win 驅動包沒有 ARM64 版本。最後是從 UTM Guest Tools 的映像裡拿到 NetKVM\w11\ARM64\netkvm.inf 才裝起來。
順帶一個小教訓:我寫的驅動安裝腳本假設光碟在 D:,但那是 Windows 安裝 ISO,guest tools 其實掛在 F:。在腳本裡寫死磁碟機代號,是一個很容易犯又很難查的錯。
nc -z 說埠是通的,但其實不通判斷 SSH 好了沒,我用了:
nc -z 127.0.0.1 2222 && echo "SSH ready"
它說通了,但 ssh 連不上。
因為 QEMU 的 hostfwd 是先在 host 這端接受連線,然後才嘗試轉發到 guest。所以 TCP 握手在 host 就完成了,nc 看到的是 QEMU,不是 sshd。
這是 Day 1 就開始講的那個主題的另一個版本:檢查了一個代理指標,而不是你真正關心的事實。正確的檢查是真的去連一次 SSH。
send-key 成功了,但什麼都沒打進去QEMU 的 QMP 介面可以送按鍵:
{"execute": "send-key", "arguments": {"keys": [...]}}
回傳 {"return": {}},成功。但畫面上的「執行」對話框裡什麼都沒有。
因為那個對話框可見,但沒有輸入焦點。
這件事應該很眼熟——它就是 Day 2 那個問題在虛擬機層級的翻版:送出成功不代表送到了你以為的地方。解法是先用 input-send-event 送一次絕對座標的滑鼠點擊,把焦點放到對話框上,再送按鍵。
還有一個更好笑的:我的鍵盤對照表漏掉了 |,於是 ipconfig | Select-String ... 變成 ipconfig Select-String ...,印出一頁說明文字。修法不只是補上那個字元,而是讓對照表在遇到不認識的字元時直接拋例外——安靜地丟掉一個字元,比拒絕執行糟糕得多。
環境搞定了,SSH 也通了,然後:
$ ssh win 'python -m pytest tests/'
WindowDiscoveryTimeoutError: Window failed to appear within 30.0s
這就是 Day 2 那篇的起點:SSH 的登入 session 沒有互動式桌面。
解法是在 guest 裡建一個排程工作(scheduled task),設定成以「已登入的那個使用者 session」執行,SSH 只負責觸發它、然後去讀輸出檔:
schtasks /Run /TN wtgui # 在 session 1 裡跑
等於是隔著一道牆借一張桌面。醜,但它讓「在本機重現 CI」這件事真的成立了。
搭這台虛擬機大概花了我一個下午。而它回收了什麼:
能互動的環境和只能看 log 的環境,差別不是快慢,是有些問題你根本問不出口。 在只能推 commit 的環境裡,你會不自覺地只提出「改這行看看會不會好」這種問題,而不是「如果只改這一個變因會怎樣」。
autounattend.xml),因為你會重灌很多次nc -z 測不出 QEMU hostfwd 後面的 guest 有沒有活著——別檢查代理指標
send-key 需要目標有輸入焦點第四章結束。明天進入第五章:驅動真實的應用程式——從 Scintilla 到 WinUI 3,四個從來沒聽過這個函式庫的開源軟體。