4274 字
21 分钟
用 KVM 虚拟机在 Linux 上跑原生 Windows 应用

一份可复现的部署记录:KVM 虚拟机跑一台通用 Windows 11(主要用途是原生 Office),Linux 桌面上表现为一个个独立的应用窗口(不是一块远程桌面)。宿主侧脚本、域、磁盘统一叫 win11。


0. 为什么是这个方案#

先排除掉不合适的路:

方案结论
Wine / CrossOver 直接跑 Office不可用。CodeWeavers 从 CrossOver 26 起停止 Office 2016 / Microsoft 365 支持(官方兼容表变成 Installs, Will Not Run),理由是微软每次更新就破一次;社区 fork 与 Bottles 实验 runner 只覆盖 Word/Excel 基础编辑,VBA、Outlook、受保护文档全在雷区
LibreOffice / OnlyOffice日常够用,但「和别人交换文档必须 1:1」这种场景不行
网页版 Office离线和大文件不行,桌面版功能缺失
KVM 虚拟机 + RDP RemoteApp本方案。真 Windows、真 Office、真 VBA;Linux 侧一个应用一个窗口

架构:Windows 虚拟机(无桌面窗口概念) → RDP RemoteApp(RAIL 协议) → FreeRDP 客户端把每个 Windows 顶层窗口映射成一个原生 X11 窗口。Windows 本体藏在后台,用户在 Linux 应用菜单里点「Word」就直接出 Word 窗口。

等价于 WinApps 的思路,但不依赖 Docker,全部用 libvirt + xfreerdp3 原生组件。


1. 前置条件与检查#

Terminal window
# CPU 虚拟化 + KVM 设备
grep -oE 'vmx|svm' /proc/cpuinfo | head -1
ls -l /dev/kvm
# 资源参考值(本机部署用 4 vCPU / 8 GB / 120 GB)
nproc; free -g; df -h /var/lib

要点:

  • 不需要 GPU 直通。Office 用 virtio/qxl 软件渲染足够,独显留在宿主跑桌面。
  • 虚拟机常驻约占 2–4 GB 内存。

2. 主机侧准备#

Terminal window
sudo apt-get update
sudo apt-get install -y qemu-kvm qemu-utils libvirt-daemon-system libvirt-clients \
virtinst virt-manager ovmf swtpm swtpm-tools virt-viewer freerdp3-x11
sudo systemctl enable --now libvirtd
# 普通用户加入组(重新登录后生效;没生效时可用 sg libvirt -c '...' 临时访问)
sudo usermod -aG libvirt,kvm "$USER"
# 介质目录:必须让 libvirt-qemu 能读到,不要放在 ~ 下
sudo mkdir -p /var/lib/libvirt/images/iso
sudo chown libvirt-qemu:libvirt /var/lib/libvirt/images/iso
# 默认 NAT 网络
virsh -c qemu:///system net-list --all # default 应为 active

验证:sg libvirt -c 'virsh -c qemu:///system list --all' 能返回空列表即为通。


3. 获取 Windows ISO(微软官方直链)#

不要用第三方镜像站。微软的下载页需要浏览器会话和反爬,但它的 software-download-connector API 可以用脚本走通(Fido 脚本的原理):先向 vlscppe.microsoft.com/tags + ov-df.microsoft.com 白名单化 session,再请求 SKU 信息与下载链接。

~/.local/share/win11/get-win11-iso.py 就是这段流程的最小实现(部署脚本都放在那个目录,本文只记录方法):

Terminal window
./get-win11-iso.py zh-CN # 参数是 locale,可用 en-US / zh-TW
# 内部 edition id:3813 = x64 客户端(3816 是 arm64,别选错)
# 输出即为 software.download.prss.microsoft.com 上的官方直链

拿到链接后:

Terminal window
curl -L -C - --retry 5 -o /var/lib/libvirt/images/iso/win11.iso '<上一步输出的 URL>'

注意:

  • 链接带签名和有效期,取到后立刻下载;过期就重跑脚本(注意会被 Sentinel 反爬拦一段时间,别连续重试,隔几小时再来)。
  • 大小为 9 GB 左右(26H2 / build 26300 量级)。校验方式:Content-Length 与本地文件大小一致即可,再 mount -o loop 看 sources/install.wim 是否在。
  • Windows ISO 的载荷几乎全在 UDF 里,xorriso -indev -ls / 只会看到 README.TXT,误以为空盘。要用 sudo mount -o loop,ro win11.iso /mnt/x 检查。

4. 无人值守安装介质#

4.1 交付方式#

autounattend.xml 放在一张单独的小 ISO(第二块光驱)里,Windows Setup 会自动在可移动介质根目录找它。同时把 post-install.ps1 一起放进去,供首次登录时调用。

Terminal window
mkdir -p ~/.local/share/win11/seed
sed -e "s|__WIN_PASS__|$WIN_VM_PASS|g" -e "s|__COMPUTER_NAME__|WIN11|g" \
~/.local/share/win11/autounattend.xml.tmpl > ~/.local/share/win11/seed/autounattend.xml
cp ~/.local/share/win11/post-install.ps1 ~/.local/share/win11/seed/
cd ~/.local/share/win11/seed && xorriso -as mkisofs -quiet -J -R -V OEMDRV \
-o /var/lib/libvirt/images/iso/win11-seed.iso .

4.2 autounattend.xml 全文#

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<SetupUILanguage>
<UILanguage>zh-CN</UILanguage>
</SetupUILanguage>
<InputLocale>0804:00000804</InputLocale>
<SystemLocale>zh-CN</SystemLocale>
<UILanguage>zh-CN</UILanguage>
<UserLocale>zh-CN</UserLocale>
</component>
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<DiskConfiguration>
<Disk wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order><Type>EFI</Type><Size>300</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>2</Order><Type>MSR</Type><Size>16</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>3</Order><Type>Primary</Type><Extend>true</Extend>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order><PartitionID>1</PartitionID><Format>FAT32</Format><Label>System</Label>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order><PartitionID>3</PartitionID><Format>NTFS</Format><Label>Windows</Label><Letter>C</Letter>
</ModifyPartition>
</ModifyPartitions>
<WillWipeDisk>true</WillWipeDisk>
<DiskID>0</DiskID>
</Disk>
<WillShowUI>OnError</WillShowUI>
</DiskConfiguration>
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Key>/IMAGE/NAME</Key>
<Value>Windows 11 Pro</Value>
</MetaData>
</InstallFrom>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>3</PartitionID>
</InstallTo>
<WillShowUI>OnError</WillShowUI>
</OSImage>
</ImageInstall>
<UserData>
<AcceptEula>true</AcceptEula>
<FullName>office</FullName>
<Organization>local</Organization>
<ProductKey>
<Key>VK7JG-NPHTM-C97JM-9MPGT-3V66T</Key>
<WillShowUI>Never</WillShowUI>
</ProductKey>
</UserData>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<ComputerName>__COMPUTER_NAME__</ComputerName>
<TimeZone>China Standard Time</TimeZone>
</component>
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<RunSynchronous>
<RunSynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>1</Order><Description>enable RDP</Description>
<Path>reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>2</Order><Description>enable RemoteApp</Description>
<Path>reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\TSAppAllowList" /v fDisabledAllowList /t REG_DWORD /d 1 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>3</Order><Description>disable NLA requirement</Description>
<Path>reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v UserAuthentication /t REG_DWORD /d 0 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>4</Order><Description>disable fast startup</Description>
<Path>reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Power" /v HiberbootEnabled /t REG_DWORD /d 0 /f</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>5</Order><Description>no sleep no hibernate</Description>
<Path>cmd /c powercfg /h off &amp; powercfg -change -standby-timeout-ac 0 &amp; powercfg -change -monitor-timeout-ac 0 &amp; powercfg -change -disk-timeout-ac 0</Path>
</RunSynchronousCommand>
<RunSynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>6</Order><Description>rdp firewall rule</Description>
<Path>powershell -NoProfile -Command "Set-NetFirewallRule -Name RemoteDesktop-UserMode-In-TCP,RemoteDesktop-UserMode-In-UDP -Enabled True -ErrorAction SilentlyContinue"</Path>
</RunSynchronousCommand>
</RunSynchronous>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<UserAccounts>
<LocalAccounts>
<LocalAccount wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Password>
<Value>__WIN_PASS__</Value>
<PlainText>true</PlainText>
</Password>
<Description>office</Description>
<DisplayName>office</DisplayName>
<Group>Administrators</Group>
<Name>office</Name>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<AutoLogon>
<Password>
<Value>__WIN_PASS__</Value>
<PlainText>true</PlainText>
</Password>
<Enabled>true</Enabled>
<LogonCount>3</LogonCount>
<Username>office</Username>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<Order>1</Order>
<Description>win11 post install</Description>
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-ChildItem D:\,E:\,F:\,G:\ -Filter post-install.ps1 -Depth 2 -ErrorAction SilentlyContinue | Select-Object -First 1 | ForEach-Object { &amp; $_.FullName }"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>

4.3 这份 XML 里每处「不这么写就不行」的地方#

  1. XSD 序列顺序。DiskConfiguration 里 Disk 必须在 WillShowUI 之前;OSImage 必须是 InstallFrom → InstallTo → WillShowUI。顺序错 → 整份答案文件被静默忽略,安装退回图形界面(表现为又出现语言页/密钥页)。
  2. International-Core-WinPE 不能省。少这个组件,即使 Setup 组件正确,安装程序仍会停在「选择语言设置」页。
  3. 产品密钥用微软公开的通用安装密钥。消费级多版本 ISO 不给密钥会停在「输入产品密钥」页。VK7JG-NPHTM-C97JM-9MPGT-3V66T 是 Win11 Pro 的公开通用安装密钥,作用是选版本 + 跳过该页,与激活无关。
  4. /IMAGE/NAME 显式指定版本。中文消费级 ISO 里 Home 系列排在前面,不指定会装成 Home——Home 不能当 RDP 服务端,整个方案作废。
  5. ModifyPartitions 不要给 MSR 分区做 Format。
  6. FirstLogonCommands 里用「扫 D:–G: 找脚本」而不是写死盘符,因为光驱盘符顺序会变。

5. 虚拟机定义#

关键片段(完整见 win11.xml.tmpl):

<os>
<type arch='x86_64' machine='q35'>hvm</type>
<!-- .ms. 版本 = 已内建微软密钥的 Secure Boot 变量 -->
<loader readonly='yes' type='pflash'>/usr/share/OVMF/OVMF_CODE_4M.ms.fd</loader>
<nvram template='/usr/share/OVMF/OVMF_VARS_4M.ms.fd'>/var/lib/libvirt/qemu/nvram/win11_VARS.fd</nvram>
</os>
<features>
<acpi/><apic/><smm state='on'/>
<hyperv mode='custom'> <!-- Windows 客户机性能,缺了会明显慢 -->
<relaxed state='on'/><vapic state='on'/><spinlocks state='on' retries='8191'/>
<vpindex state='on'/><runtime state='on'/><synic state='on'/><stimer state='on'/>
<reset state='on'/><frequencies state='on'/><tlbflush state='on'/><ipi state='on'/>
</hyperv>
</features>
<cpu mode='host-passthrough' check='none' migratable='on'/>
<clock offset='localtime'>
<timer name='rtc' tickpolicy='catchup'/>
<timer name='hpet' present='no'/>
<timer name='hypervclock' present='yes'/>
</clock>

设备侧:

<!-- 系统盘:SATA(AHCI)——Windows 自带驱动,免装驱动即可安装 -->
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' cache='none' discard='unmap'/>
<source file='/var/lib/libvirt/images/win11.qcow2'/>
<target dev='sda' bus='sata'/>
<boot order='1'/>
</disk>
<!-- 安装 ISO:放第二位,装完不删也不影响(无按键即跳过) -->
<disk type='file' device='cdrom'> ... <target dev='sdb' bus='sata'/><readonly/><boot order='2'/></disk>
<!-- 无人值守介质 -->
<disk type='file' device='cdrom'> ... <target dev='sdc' bus='sata'/><readonly/></disk>
<!-- 网卡:e1000e 同样是 Windows 内置驱动;用 virtio-net 就得先灌驱动 -->
<interface type='network'><source network='default'/><model type='e1000e'/></interface>
<tpm model='tpm-crb'><backend type='emulator' version='2.0'/></tpm>
<graphics type='spice' autoport='yes' listen='127.0.0.1'/>
<video><model type='qxl' ram='65536' vram='65536' vgamem='32768' heads='1' primary='yes'/></video>

为什么不用 virtio:virtio-blk/virtio-net 需要事先把 virtio-win 驱动塞进安装介质(DriverPaths 或 vioscsi 技巧),多一条出错链。Office 场景 SATA + e1000e 没有任何可感知差异。以后要提速再挂 virtio-win.iso 装驱动、改 3 行 XML 即可。


6. 一键建机与安装#

~/.local/share/win11/media.conf
# WIN_ISO=/var/lib/libvirt/images/iso/win11.iso
sudo -v
sg libvirt -c ~/.local/share/win11/create-vm.sh

create-vm.sh 做四件事:生成随机密码存 credentials.env(600)→ 渲染 autounattend.xml 并打成 seed ISO → 建 120 GB qcow2 → virsh define + start。

6.1 引导时的两个必知点#

  • 「Press any key to boot from CD or DVD」:Windows 安装介质默认要按键才引导。自动化时用
    Terminal window
    for i in $(seq 1 30); do
    sg libvirt -c 'virsh -c qemu:///system send-key win11 KEY_SPACE' >/dev/null; sleep 0.4
    done
    装完重启后不要再按键,它会自动回落到硬盘引导。
  • 进度观察(纯无头也能看):
    Terminal window
    virsh -c qemu:///system screenshot win11 /tmp/wo.ppm # 实际是 PNG
    文件大小是个廉价信号:几 KB ≈ 文本/黑屏,几百 KB–1 MB ≈ 图形界面。

全过程约 25–35 分钟(装系统 15 分钟 + Office 在线安装 8–12 分钟),中间不需要任何点击。


7. 安装后置:post-install.ps1#

首次登录(AutoLogon)后由 FirstLogonCommands 执行,运行在提权上下文,所以能直接写 HKLM。它做两件事:

  1. RDP + RemoteApp 解锁:
    • Terminal Server\fDenyTSConnections = 0
    • Terminal Server\TSAppAllowList\fDisabledAllowList = 1 ← 没有这一条,RAIL 的 /app: 会直接被服务端拒绝
    • RDP-Tcp\UserAuthentication = 0(关掉 NLA 要求,客户端用密码直连更省事)
    • Set-NetFirewallRule -Name RemoteDesktop-UserMode-In-TCP/-In-UDP -Enabled True(用规则名而不是显示名,避免中文系统本地化差异)
  2. Microsoft 365 在线安装:向 c2rsetup.officeapps.live.com 的官方引导地址取安装器(6.8 MB 引导器,运行后自己下 ~2.5 GB 载荷),静默安装 O365HomePremRetail(个人版/家庭版);商业版是 O365ProPlusRetail。

脚本里两个细节值得保留:

  • 先等网络(轮询 c2rsetup.officeapps.live.com:443,最多 120 秒),否则首次登录时 DHCP 还没好,下载直接失败。
  • 两轮重试 + 校验 WINWORD.EXE 是否存在,装完把结果和 ready.txt 落在 C:\win11\。

必踩坑:给 Windows 用的 .ps1 必须带 UTF-8 BOM。 PowerShell 5.1 在无 BOM 时按 ANSI 解码,中文注释会吞掉后面的代码行,症状是变量莫名变 $null、Test-Path 参数绑定失败、注册表写入报 PermissionDenied,而且不报错。写文件时用 encoding='utf-8-sig'(Python)或 :set bomb(vim)。


8. Linux 侧:把一个 Windows 程序变成原生窗口#

8.1 最小验证#

Terminal window
xfreerdp3 /v:<guest-ip> /u:office /p:<pass> /d: /cert:ignore \
/app:"program:C:\Windows\System32\notepad.exe"

就会弹出一个标题为「无标题 - 记事本」的原生 X11 窗口。

三个语法要点:

  • /app: 的值是 program:<完整 exe 路径>。
  • 不要加 || 前缀(即别写 program:||C:\...)。加了会被当成字面路径,服务端返回 RAIL_EXEC_E_FILE_NOT_FOUND (NtError=0x2)。
  • 路径含空格时整体用双引号包住 /app:"program:C:\Program Files\...",不要再在内部加引号。
  • /sec:tls 往往是必需的。UserAuthentication=0 只是让服务端「接受」TLS,但 FreeRDP 3 默认仍先按 NLA 发起;guest 侧 CredSSP 这条路不通时,会在 nla_recv_pdu 处报 ERRCONNECT_LOGON_FAILURE [0x00020014] —— 看起来像密码错,实际换个认证层就通。补上 /sec:tls 即可(仍是 TLS 加密,NAT 内网可接受)。
  • 断连 ≠ 失败。会话被别的连接接管或正常关窗时,xfreerdp3 退出码同样非 0、日志里同样有 [ERROR]。判断「真失败」要语义化:日志里没有 gdi_init_ex / Logon Info V2 才算出错,否则会满屏假警报。

多个应用不能并发 —— 这是 Windows 客户端的硬限制,不是客户端的问题:Windows 11 客户端版(Pro 也一样)只允许一个交互式 RDP 会话,第二个连接会把第一个从服务端踢掉,旧会话收到 ERRINFO_RPC_INITIATED_DISCONNECT(“initiated by an administrative tool on the server in another session”)。实测依次启动 Excel → Outlook → Word,每个都登录成功、窗口正常弹出,但前一个在 10–40 秒内被踢下线。所谓「RemoteApp 能多会话」是 Windows Server / RDS 才有的行为。

单会话下的三种编排:

编排做法代价
串行(默认)每个应用一个图标,一次用一个点新图标会关掉旧应用,未保存文档会丢 → 启动器里加一步 zenity 确认
会话宿主只连一次,/app: 给 explorer.exe 或 PowerShell,从里面再启动 Office点图标不再直达应用,需两步
真并发换 Windows Server(RDS)许可成本;RDPWrap 一类多会话补丁违反客户端许可,24H2 上还频繁失效

关键一点:/app: 只决定自动启动哪个程序,不限制一个会话里跑多少程序 —— RAIL 会把这个会话内所有顶层窗口都映射成原生窗口。所以「会话宿主」编排理论上也能把 Word/Excel/PPT 全变成独立窗口(需实测确认,因为客户端只监视被明确要求的窗口集)。

8.2 启动器脚本#

win11-run 接受应用名或任意 exe 路径,负责:VM 没开就开 → 从 libvirt DHCP 租约拿 guest IP → 等 3389 可连 → 起 xfreerdp3。

Terminal window
# 关键片段
VQ() { sg libvirt -c "virsh -c qemu:///system $*" 2>/dev/null; } # 组权限没生效时的兜底
vm_running() { VQ domstate "$VM" | grep -q running; }
guest_ip() { VQ domifaddr "$VM" --source lease | awk '/ipv4/ && $4 ~ /^192\.168\./ {split($4,a,"/"); print a[1]; exit}'; }
declare -A APPS=(
[word]='C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE'
[excel]='C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE'
[powerpoint]='C:\Program Files\Microsoft Office\root\Office16\POWERPNT.EXE'
[outlook]='C:\Program Files\Microsoft Office\root\Office16\OUTLOOK.EXE'
[explorer]='C:\Windows\explorer.exe'
)
exec xfreerdp3 /v:"$ip" /u:"$user" /p:"$pass" /d: /cert:ignore /sec:tls \
/dynamic-resolution /clipboard +auto-reconnect \
/app:"program:${APPS[$target]:-$target}"

两个实测出来的补充(完整脚本见 ~/.local/share/win-office/win-office-run):

  • /sec:tls 不能省,否则默认 NLA 路径会在 nla_recv_pdu 死掉。
  • 起客户端之前先检查有没有别的 xfreerdp3 在跑,有就弹 zenity 确认再 kill -TERM + kill -9(因为新会话会把它踢掉,不收拾就留孤儿窗口)。退出码非 0 不一定算失败,见 §8.1 最后一条。

冷启动链路:点图标 → VM 从关机状态拉起 → 约 60 秒出窗口(Windows 启动占大头)。

8.3 桌面图标#

install-launchers.sh 在 ~/.local/share/applications/win11/ 生成 5 个 .desktop。图标用系统主题里确定存在的(libreoffice-writer/calc/impress、internet-mail、folder),不要指望 x-office-* 这类旧名在所有主题里都有。


9. 排障手册#

9.1 一个可复用的「无头排障通道」#

把自己的工作目录通过 RDP 磁盘重定向映射进 Windows,然后起一个 RemoteApp PowerShell:

Terminal window
xfreerdp3 /v:"$ip" /u:"$user" /p:"$pass" /d: /cert:ignore /clipboard /drive:host,"$HOME/.local/share/win11" \
/app:"program:C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"

Windows 里就能 \\tsclient\host\xxx.ps1 读写宿主文件 → 脚本和日志双向流转,不用截图读文本。这是排查的主力手段。

驱动输入时注意两件事:

  • 中文 IME 会把 xdotool type 打的反斜杠变成中文候选(\tsclient 打成 敌人、同时client)。可靠做法:xclip -selection clipboard < 命令文件,然后在窗口里 Ctrl+V。
  • xdotool windowactivate 前先确认真实窗口标题;焦点了浏览器会把命令粘到地址栏。

9.2 窗口卡死关不掉(RAIL:0006020E 之类)#

现象:一个标题像 RAIL:xxxx 或空标题的窗口,点叉没反应。

原因:FreeRDP 客户端进程僵死,占着若干孤儿窗口,连 SIGTERM 都不理。

Terminal window
pgrep -ax xfreerdp3 # 找 PID
kill -9 <pid> # SIGTERM 无效时直接 -9

不建议用 pkill -f <模式>:模式串常常也出现在你自己这条命令行里,会把自己的 shell 一起杀掉(表现为命令返回 143)。同理,排查时不要对同一个 Windows 会话开第二个「整桌面」RDP 连接——它会挤掉正在用的那个,并留下孤儿窗口。

9.3 Office 登录卡在「正在加载…」#

不是网络问题(先确证:Invoke-WebRequest https://login.microsoftonline.com/ 返回 200 即可排除 DNS/代理)。真实原因是这些对话框以独立顶层窗口出现,RAIL 有时先不映射它们:

  1. 《接受许可协议》——Office 首次运行的第一道弹窗,它没被映射出来时,Word 看起来就像死在那儿;
  2. 登录容器本身。

处理:等它把弹窗映射出来(一般十几秒到一分钟)、依次点掉协议与隐私设置,再登录。判断是否已登录成功,最直接的证据是 Word 开始页「最近」列表刷出了 OneDrive 里的文件。

9.4 其他#

症状处理
Failed to open ... Permission denied(seed ISO 在 ~ 下)介质放到 /var/lib/libvirt/images/iso/;libvirt-qemu 进不去家目录
xorriso FAILURE ... Permission denied 重新打包时上次生成的 ISO 已被 libvirt chown 给 qemu,先 rm -f 再打
安装程序停在语言页/密钥页见 4.3 的 1、2、3 条
装完每次重启都等 5 秒正常:安装光盘仍挂在第一位,无按键即回落硬盘。想彻底避免就把 <boot order> 交换

10. 已知限制#

  • Windows 未激活:右下角水印、不能改外观,其余功能与更新正常,不会强制关机。要激活就在设置里输密钥或用绑定微软账号的数字授权。
  • 会话与并发的边界:Windows 客户端版只支持一个 RDP 会话。既不能 RemoteApp 多应用并发,也不能再叠加一个「整桌面 RDP 连接」——新连接一律踢掉旧会话(ERRINFO_RPC_INITIATED_DISCONNECT)。被踢的那一端进程常常不退出,只留下点不掉的孤儿窗口,见 §9.2。想真并发只能上 Windows Server / RDS。
  • Windows 更新可能在你用时重启虚拟机。要稳就改成仅通知。
  • 365 登录只需做一次(令牌留在 VM 的用户配置里,之后重启依然有效);换 VM 就得重来。
  • Office 装在 VM 里算一台设备,受订阅的 5 台设备上限约束。

11. 文件清单(~/.local/share/win11/)#

文件作用
get-win11-iso.py走微软官方 API 取 Windows 11 ISO 直链
media.confWindows ISO 路径(Office 走在线安装,无需 ISO)
autounattend.xml.tmpl无人值守答案文件模板(含 __WIN_PASS__ / __COMPUTER_NAME__ 占位)
post-install.ps1首次登录后置:RDP/RemoteApp/电源 + 365 静默安装(UTF-8 BOM)
create-vm.sh生成 seed ISO → 建盘 → 渲染 XML → 定义并启动
win11.xml.tmpl域定义模板
credentials.env生成的 Windows 账户与密码(600,勿外传)
win11-runRemoteApp 启动器(应用名 → 原生窗口)
install-launchers.sh生成 5 个桌面图标
rdp-desktop整桌面窗口(一次性调试/排障用,不要与日常窗口同时用)
win11-shell带宿主共享盘的 RemoteApp PowerShell(排障主力)
seed/渲染后的答案文件与脚本(打包用)

重建流程:删掉 win11.qcow2、credentials.env,跑 create-vm.sh,然后按 §6.1 发按键;装完执行一次 install-launchers.sh。


12. 与上游方案的关系#

WinApps 做的是同一件事,但把 FreeRDP 包进 Docker 容器里再加一层 winapps 命令行。本方案省掉容器层,代价是所有细节要自己掌握——也就是本文档存在的理由。若哪天需要 winapps 的模块化配置(多应用清单、/app: 参数自动推导),可以迁过去,虚拟机和 Windows 侧配置全部复用。

用 KVM 虚拟机在 Linux 上跑原生 Windows 应用
https://fenlyin.dpdns.org/posts/kvm-windows-remoteapp/
作者
Hiyuki
发布于
2026-10-26
许可协议
CC BY-NC-SA 4.0