windows-vm
Operate a Windows guest under libvirt from the Linux host without a human relaying screens — find the VM and its disks, start and stop it safely, take screenshots, type and click, and run PowerShell t
Files1
View on GitHubSKILL.mdskills/windows-vm/SKILL.md
GitHubwindows-vm
Every command here talks to the system libvirt instance. Set it once per shell:
export LIBVIRT_DEFAULT_URI=qemu:///system
Find the VM
virsh list --all # name and state of every domain
virsh domblklist <dom> --details # its disks: Type "block" is a physical disk
virsh dumpxml <dom> # full definition
virsh qemu-agent-command <dom> '{"execute":"guest-ping"}' # {"return":{}} = guest agent is up
A domain whose disk has Type block and points at /dev/disk/by-id/..., or at a
/dev/mapper/... device built from a real disk's partitions, boots a real disk, often the
same Windows install the machine also boots natively. Treat its disks as
Windows' own: never mount, dd, ntfsfix or partition them while virsh domstate <dom>
says anything other than shut off. Two writers on one NTFS volume corrupt it.
Start and stop
virsh start <dom>
virt-manager --connect qemu:///system --show-domain-console <dom> # window for the user
A start can be refused by a libvirt prepare hook (virsh start prints its message):
a Windows partition mounted on the host, a hibernated or unclean volume, another
process holding the disk, or a partition table the guest changed (partition changes belong
in native Windows). Report the message; don't work around the check. A hibernated
or unclean volume clears only after Windows boots natively and shuts down.
To stop, ask Windows to shut down and wait for it:
virsh shutdown <dom> --mode agent || virsh shutdown <dom> --mode acpi
for _ in $(seq 60); do [ "$(virsh domstate <dom>)" = "shut off" ] && break; sleep 5; done
virsh destroy is pulling the power cord. Use it only on a guest that is hung (frozen
screenshot, agent not answering) and say that you did.
See the screen
virsh screenshot <dom> --file /tmp/<dom>.ppm
nix run nixpkgs#imagemagick -- /tmp/<dom>.ppm /tmp/<dom>.png
Then read the PNG. Take one after every action that changes the screen: boot, a click,
a dialog. Compare md5sum of successive shots to tell a hung screen from a slow one
without reading every image.
Keyboard
Keys are Linux keycode names; keys given in one call are pressed together.
virsh send-key <dom> KEY_ENTER
virsh send-key <dom> KEY_LEFTCTRL KEY_LEFTALT KEY_DELETE # Ctrl+Alt+Del
virsh send-key <dom> KEY_LEFTMETA KEY_R # Win+R
virsh send-key <dom> KEY_LEFTSHIFT KEY_A # capital A
Text goes one send-key per character, so keep it short (a Run box command, a search
term). Anything longer goes through the guest agent. Windows boot and recovery screens
(F8 startup settings, "Press Enter to try again") take keys too.
Mouse
The VM has a USB tablet, so positions are absolute: 0–32767 on each axis, whatever the
resolution. Scale from the screenshot: x = px * 32767 / width.
virsh qemu-monitor-command <dom> '{"execute":"input-send-event","arguments":{"events":[
{"type":"abs","data":{"axis":"x","value":16384}},
{"type":"abs","data":{"axis":"y","value":16384}}]}}'
virsh qemu-monitor-command <dom> '{"execute":"input-send-event","arguments":{"events":[
{"type":"btn","data":{"down":true,"button":"left"}}]}}'
virsh qemu-monitor-command <dom> '{"execute":"input-send-event","arguments":{"events":[
{"type":"btn","data":{"down":false,"button":"left"}}]}}'
Run commands (guest agent)
Needs the QEMU guest agent service in Windows (from virtio-win) and a
org.qemu.guest_agent.0 channel in the domain. Commands run as SYSTEM in session 0: no
desktop, no user profile, no visible windows. Use the screen for anything the user should
see.
guest-exec starts the process and returns a pid; guest-exec-status reports it, with
output base64-encoded:
ps1='Get-Volume | Format-Table -AutoSize | Out-String -Width 200'
pid=$(virsh qemu-agent-command <dom> "$(jq -nc --arg c "$ps1" '{execute:"guest-exec",arguments:{
path:"powershell.exe",arg:["-NoProfile","-NonInteractive","-Command",$c],"capture-output":true}}')" \
| jq .return.pid)
until st=$(virsh qemu-agent-command <dom> "{\"execute\":\"guest-exec-status\",\"arguments\":{\"pid\":$pid}}") \
&& [ "$(jq .return.exited <<<"$st")" = true ]; do sleep 1; done
jq -r '.return["out-data"] // "" | @base64d' <<<"$st"
jq -r '.return["err-data"] // "" | @base64d' <<<"$st"
jq .return.exitcode <<<"$st"
Other agent calls: guest-get-osinfo, guest-get-fsinfo, guest-network-get-interfaces,
guest-file-open/-read/-write/-close for files without a shell.
Without the agent
Before virtio-win is installed there is no agent: drive the screen. Win+R, type
powershell, Enter, then type a short command. Install the agent first when the task is
more than a few commands; the virtio-win ISO comes from
https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
(attach it with virsh change-media or virsh attach-disk ... --type cdrom), and its
virtio-win-guest-tools.exe installs the drivers, the guest agent and the SPICE agent
in one go.