Files
cloudhost/run-windows11.sh
T
2026-08-26 12:36:30 +08:00

193 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
state_dir="${WIN11_VM_STATE_DIR:-${script_dir}/.vm/windows11}"
disk_image="${WIN11_VM_DISK_IMAGE:-${state_dir}/disk.qcow2}"
seed_iso="${WIN11_VM_SEED_ISO:-$(dirname -- "$disk_image")/seed.iso}"
disk_size="${WIN11_VM_DISK_SIZE:-64G}"
memory="${WIN11_VM_MEMORY:-8G}"
cpus="${WIN11_VM_CPUS:-8}"
rdp_port="${WIN11_VM_RDP_PORT:-3389}"
ssh_port="${WIN11_VM_SSH_PORT:-2222}"
uefi_code="${WIN11_VM_UEFI_CODE:-/usr/share/OVMF/OVMF_CODE_4M.fd}"
uefi_vars_template="${WIN11_VM_UEFI_VARS:-/usr/share/OVMF/OVMF_VARS_4M.fd}"
uefi_vars="${state_dir}/OVMF_VARS_4M.fd"
pid_file="${state_dir}/qemu.pid"
log_file="${state_dir}/console.log"
monitor_socket="${state_dir}/qemu-monitor.sock"
guest_agent_socket="${state_dir}/qemu-guest-agent.sock"
daemonize=false
install_from_iso=false
extra_args=()
usage() {
cat <<EOF
Usage: $(basename "$0") [--daemon] [--install-from-iso] [-- QEMU_ARGUMENT ...]
If ${disk_image} is missing, create it from the Windows 11 QCOW2 base image.
Use --install-from-iso to perform an automatic ISO installation instead.
If it exists, start Windows normally without attaching installation media.
Options:
--daemon Run QEMU in the background
--install-from-iso Install from ISO when the system disk is missing
-h, --help Show this help
Environment variables:
WIN11_VM_BASE_IMAGE Windows 11 base QCOW2 image (default: beside shared images)
WIN11_VM_INSTALL_ISO Windows 11 installation ISO used with --install-from-iso
WIN11_VM_TOOLS_ISO VirtIO Windows tools ISO (default: resolved by shared image helper)
WIN11_VM_MEMORY Guest memory (default: 8G)
WIN11_VM_CPUS Virtual CPU count (default: 8)
WIN11_VM_RDP_PORT Host RDP port (default: 3389)
WIN11_VM_SSH_PORT Host SSH port (default: 2222)
WIN11_VM_STATE_DIR Runtime directory (default: .vm/windows11)
WIN11_VM_DISK_IMAGE System disk path
WIN11_VM_DISK_SIZE New system disk size (default: 64G)
WIN11_VM_SEED_ISO Answer-file ISO path (default: alongside the VM disk)
WIN11_VM_UEFI_CODE OVMF read-only firmware path
WIN11_VM_UEFI_VARS OVMF variable-store template path
EOF
}
while (($#)); do
case "$1" in
--daemon) daemonize=true; shift ;;
--install-from-iso) install_from_iso=true; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; extra_args+=("$@"); break ;;
*) printf 'Error: unknown option: %s\n' "$1" >&2; usage >&2; exit 2 ;;
esac
done
installing=false
if [[ ! -f "$disk_image" ]]; then
installing=true
if [[ -n "${VM_IMAGE_HELPER:-}" ]]; then
image_downloader="$VM_IMAGE_HELPER"
elif [[ -x "${script_dir}/vm-images/download-images.sh" ]]; then
image_downloader="${script_dir}/vm-images/download-images.sh"
else
image_downloader="${script_dir}/../vm-images/download-images.sh"
fi
[[ -x "$image_downloader" ]] || { printf 'Error: shared image helper is unavailable: %s\n' "$image_downloader" >&2; exit 1; }
if [[ "$install_from_iso" == true ]]; then
install_iso="${WIN11_VM_INSTALL_ISO:-$("$image_downloader" --print-path windows11 --iso)}"
uses_shared_tools_iso=false
if [[ -n "${WIN11_VM_TOOLS_ISO:-}" ]]; then
tools_iso="$WIN11_VM_TOOLS_ISO"
else
tools_iso="$("$image_downloader" --print-path virtio-win)"
uses_shared_tools_iso=true
fi
else
base_image="${WIN11_VM_BASE_IMAGE:-$("$image_downloader" --print-path windows11)}"
fi
fi
for command_name in qemu-system-x86_64; do
command -v "$command_name" >/dev/null 2>&1 || { printf 'Error: %s is not installed.\n' "$command_name" >&2; exit 1; }
done
if [[ "$installing" == true ]]; then
command -v qemu-img >/dev/null 2>&1 || { printf 'Error: qemu-img is not installed.\n' >&2; exit 1; }
if [[ "$install_from_iso" == true ]]; then
[[ -f "$install_iso" ]] || { printf 'Error: Windows 11 ISO was not found: %s\n' "$install_iso" >&2; exit 1; }
else
[[ -f "$base_image" ]] || { printf 'Error: Windows 11 base QCOW2 image was not found: %s\n' "$base_image" >&2; exit 1; }
fi
fi
for firmware_file in "$uefi_code" "$uefi_vars_template"; do
[[ -f "$firmware_file" ]] || { printf 'Error: required UEFI firmware was not found: %s\n' "$firmware_file" >&2; exit 1; }
done
mkdir -p "$state_dir"
if [[ "$installing" == true && "$install_from_iso" == true ]]; then
if [[ ! -f "$tools_iso" ]]; then
if [[ "$uses_shared_tools_iso" == true ]]; then
"$image_downloader" virtio-win
else
printf 'Error: VirtIO Windows tools ISO was not found: %s\n' "$tools_iso" >&2
exit 1
fi
fi
[[ -f "$tools_iso" ]] || { printf 'Error: downloader did not create: %s\n' "$tools_iso" >&2; exit 1; }
if [[ ! -f "$seed_iso" ||
"${script_dir}/seediso/windows11/Autounattend.xml" -nt "$seed_iso" ||
"${script_dir}/seediso/windows11/Configure-Windows.ps1" -nt "$seed_iso" ]]; then
"${script_dir}/seediso/windows11/build-seed.sh" "$seed_iso"
fi
fi
if [[ -f "$pid_file" ]]; then
old_pid="$(<"$pid_file")"
if [[ "$old_pid" =~ ^[0-9]+$ ]] && kill -0 "$old_pid" 2>/dev/null; then
printf 'Error: Windows 11 VM is already running (PID %s).\n' "$old_pid" >&2
exit 1
fi
rm -f "$pid_file"
fi
rm -f "$monitor_socket" "$guest_agent_socket"
[[ -f "$uefi_vars" ]] || cp -- "$uefi_vars_template" "$uefi_vars"
if [[ "$installing" == true ]]; then
if [[ "$install_from_iso" == true ]]; then
qemu-img create -q -f qcow2 "$disk_image" "$disk_size"
else
qemu-img create -q -f qcow2 -F qcow2 -b "$base_image" "$disk_image" "$disk_size"
fi
printf 'Created Windows 11 system disk: %s\n' "$disk_image"
fi
qemu_args=(
-name windows11 -machine q35 -m "$memory" -smp "$cpus"
-display gtk
# -display none -spice port=5900,addr=127.0.0.1,disable-ticketing=on,agent-mouse=off -device virtio-vga
-drive "if=pflash,format=raw,readonly=on,file=${uefi_code}"
-drive "if=pflash,format=raw,file=${uefi_vars}"
-drive "file=${disk_image},if=none,id=system_disk,format=qcow2"
-device "ide-hd,drive=system_disk,bus=ide.0,bootindex=1"
-nic "user,model=e1000e,hostfwd=tcp::${rdp_port}-:3389,hostfwd=tcp::${ssh_port}-:22"
-device "virtio-serial-pci,id=virtio_serial"
-chardev "qemu-vdagent,id=vdagent,name=vdagent,clipboard=on,mouse=off"
-device "virtserialport,chardev=vdagent,name=com.redhat.spice.0"
# Register the USB tablet after vdagent so QEMU selects it as the active pointer.
-device "qemu-xhci,id=usb_controller" -device "usb-tablet,bus=usb_controller.0"
-chardev "socket,path=${guest_agent_socket},server=on,wait=off,id=qga"
-device "virtserialport,chardev=qga,name=org.qemu.guest_agent.0"
-monitor "unix:${monitor_socket},server=on,wait=off"
)
if [[ "$installing" == true && "$install_from_iso" == true ]]; then
qemu_args+=(
-drive "file=${install_iso},if=none,id=install_media,media=cdrom,readonly=on"
-device "ide-cd,drive=install_media,bus=ide.1,bootindex=2"
-drive "file=${seed_iso},if=none,id=answer_media,media=cdrom,readonly=on"
-device "ide-cd,drive=answer_media,bus=ide.2,bootindex=3"
-drive "file=${tools_iso},if=none,id=tools_media,media=cdrom,readonly=on"
-device "ide-cd,drive=tools_media,bus=ide.3,bootindex=4"
)
fi
if [[ -r /dev/kvm && -w /dev/kvm ]]; then
qemu_args+=(-enable-kvm -cpu host)
accelerator=KVM
else
qemu_args+=(-accel tcg -cpu max)
accelerator=TCG
fi
if [[ "$installing" == true && "$install_from_iso" == true ]]; then
printf 'Starting Windows 11 automatic installation with %s acceleration.\n' "$accelerator"
elif [[ "$installing" == true ]]; then
printf 'Starting Windows 11 from the QCOW2 base image with %s acceleration.\n' "$accelerator"
else
printf 'Starting Windows 11 normally with %s acceleration.\n' "$accelerator"
fi
printf 'Account: guofang / password; RDP: localhost:%s; SSH: localhost:%s\n' "$rdp_port" "$ssh_port"
if [[ "$daemonize" == true ]]; then
: >"$log_file"
qemu_args+=(-serial "file:${log_file}" -daemonize -pidfile "$pid_file")
qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
printf 'VM started (PID %s); console log: %s\n' "$(<"$pid_file")" "$log_file"
else
exec qemu-system-x86_64 "${qemu_args[@]}" "${extra_args[@]}"
fi