一份可复现的部署记录: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. 前置条件与检查
# CPU 虚拟化 + KVM 设备grep -oE 'vmx|svm' /proc/cpuinfo | head -1ls -l /dev/kvm
# 资源参考值(本机部署用 4 vCPU / 8 GB / 120 GB)nproc; free -g; df -h /var/lib要点:
- 不需要 GPU 直通。Office 用 virtio/qxl 软件渲染足够,独显留在宿主跑桌面。
- 虚拟机常驻约占 2–4 GB 内存。
2. 主机侧准备
sudo apt-get updatesudo apt-get install -y qemu-kvm qemu-utils libvirt-daemon-system libvirt-clients \ virtinst virt-manager ovmf swtpm swtpm-tools virt-viewer freerdp3-x11sudo systemctl enable --now libvirtd
# 普通用户加入组(重新登录后生效;没生效时可用 sg libvirt -c '...' 临时访问)sudo usermod -aG libvirt,kvm "$USER"
# 介质目录:必须让 libvirt-qemu 能读到,不要放在 ~ 下sudo mkdir -p /var/lib/libvirt/images/isosudo 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 就是这段流程的最小实现(部署脚本都放在那个目录,本文只记录方法):
./get-win11-iso.py zh-CN # 参数是 locale,可用 en-US / zh-TW# 内部 edition id:3813 = x64 客户端(3816 是 arm64,别选错)# 输出即为 software.download.prss.microsoft.com 上的官方直链拿到链接后:
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 一起放进去,供首次登录时调用。
mkdir -p ~/.local/share/win11/seedsed -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.xmlcp ~/.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 & powercfg -change -standby-timeout-ac 0 & powercfg -change -monitor-timeout-ac 0 & 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 { & $_.FullName }"</CommandLine> <RequiresUserInput>false</RequiresUserInput> </SynchronousCommand> </FirstLogonCommands> </component> </settings></unattend>4.3 这份 XML 里每处「不这么写就不行」的地方
- XSD 序列顺序。
DiskConfiguration里Disk必须在WillShowUI之前;OSImage必须是InstallFrom → InstallTo → WillShowUI。顺序错 → 整份答案文件被静默忽略,安装退回图形界面(表现为又出现语言页/密钥页)。 International-Core-WinPE不能省。少这个组件,即使 Setup 组件正确,安装程序仍会停在「选择语言设置」页。- 产品密钥用微软公开的通用安装密钥。消费级多版本 ISO 不给密钥会停在「输入产品密钥」页。
VK7JG-NPHTM-C97JM-9MPGT-3V66T是 Win11 Pro 的公开通用安装密钥,作用是选版本 + 跳过该页,与激活无关。 /IMAGE/NAME显式指定版本。中文消费级 ISO 里 Home 系列排在前面,不指定会装成 Home——Home 不能当 RDP 服务端,整个方案作废。ModifyPartitions不要给 MSR 分区做 Format。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. 一键建机与安装
# WIN_ISO=/var/lib/libvirt/images/iso/win11.iso
sudo -vsg libvirt -c ~/.local/share/win11/create-vm.shcreate-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); dosg libvirt -c 'virsh -c qemu:///system send-key win11 KEY_SPACE' >/dev/null; sleep 0.4done - 进度观察(纯无头也能看):
文件大小是个廉价信号:几 KB ≈ 文本/黑屏,几百 KB–1 MB ≈ 图形界面。
Terminal window virsh -c qemu:///system screenshot win11 /tmp/wo.ppm # 实际是 PNG
全过程约 25–35 分钟(装系统 15 分钟 + Office 在线安装 8–12 分钟),中间不需要任何点击。
7. 安装后置:post-install.ps1
首次登录(AutoLogon)后由 FirstLogonCommands 执行,运行在提权上下文,所以能直接写 HKLM。它做两件事:
- RDP + RemoteApp 解锁:
Terminal Server\fDenyTSConnections = 0Terminal Server\TSAppAllowList\fDisabledAllowList = 1← 没有这一条,RAIL 的/app:会直接被服务端拒绝RDP-Tcp\UserAuthentication = 0(关掉 NLA 要求,客户端用密码直连更省事)Set-NetFirewallRule -Name RemoteDesktop-UserMode-In-TCP/-In-UDP -Enabled True(用规则名而不是显示名,避免中文系统本地化差异)
- 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 最小验证
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。
# 关键片段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:
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 都不理。
pgrep -ax xfreerdp3 # 找 PIDkill -9 <pid> # SIGTERM 无效时直接 -9不建议用 pkill -f <模式>:模式串常常也出现在你自己这条命令行里,会把自己的 shell 一起杀掉(表现为命令返回 143)。同理,排查时不要对同一个 Windows 会话开第二个「整桌面」RDP 连接——它会挤掉正在用的那个,并留下孤儿窗口。
9.3 Office 登录卡在「正在加载…」
不是网络问题(先确证:Invoke-WebRequest https://login.microsoftonline.com/ 返回 200 即可排除 DNS/代理)。真实原因是这些对话框以独立顶层窗口出现,RAIL 有时先不映射它们:
- 《接受许可协议》——Office 首次运行的第一道弹窗,它没被映射出来时,Word 看起来就像死在那儿;
- 登录容器本身。
处理:等它把弹窗映射出来(一般十几秒到一分钟)、依次点掉协议与隐私设置,再登录。判断是否已登录成功,最直接的证据是 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.conf | Windows 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-run | RemoteApp 启动器(应用名 → 原生窗口) |
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 侧配置全部复用。