Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
ci / linux (clang) (push) Canceled after 0s
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s

Build
  * cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
    winpthreads and ncurses 6.6 are linked statically, so the release is
    cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
  * windres embeds a manifest with activeCodePage=UTF-8 and a version resource
  * ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
    wide-char declarations): scripts/patches/
  * scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
    UCRT64; PR #1's MINGW64 notes now point there
  * vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW

UTF-8
  * paths cross the system call boundary as UTF-16 (*W APIs); console code
    pages set to 65001 and restored on exit; files are read and written in
    binary mode
  * child output that is not valid UTF-8 is decoded from the console, OEM or
    ANSI system code page
  * read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
    endings and a UTF-8 BOM

OS layer and processes
  * os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
    subagent, plugins, skills, undo and fs_tools; realpath / path_under and
    symlink rejection work on Windows
  * every child goes through CreateProcessW with its own environment block
    inside a Job Object, so timeouts and Esc end the whole process tree

Sandbox (auto mode, "auto · lowil")
  * run_shell runs with a restricted Low integrity token; the workspace gets
    an inheritable Low label once, the sandbox TEMP is labeled Low, and the
    credentials file gets a no-read-up label
  * drive roots, the profile and system folders are never labeled
  * network commands still ask, since Low IL does not block the network;
    Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
  * /sandbox lists labeled folders, /sandbox reset-labels restores them

TUI
  * raster console fonts are switched to Consolas / NSimSun while running
  * the home logo falls back to text when the console font lacks U+2580
  * mouse drag copy goes to the Windows clipboard

Distribution
  * scripts/install_windows.ps1 installs a Gitea release into
    %LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
    cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
    zip with the token taken from the environment
  * scripts/run_windows.bat is gone; docs/README-windows.md is rewritten

Tests
  * new test_lowil, test_win_utf8 and test_os_dir; tests made portable
    (cst_test_util.h)
  * scripts/win_test.sh and scripts/winvm/ run the suite on English and
    Chinese Windows Server VMs as Administrator, a standard user and a
    Chinese user name: 29/29 on all six
This commit is contained in:
WHC
2026-10-06 23:08:14 +08:00
parent 9862c29f58
commit 2a43ae6622
84 changed files with 9784 additions and 1083 deletions
+98
View File
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>en-US</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<DiskConfiguration>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Type>Primary</Type>
<Extend>true</Extend>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order>
<PartitionID>1</PartitionID>
<Active>true</Active>
<Format>NTFS</Format>
<Label>Windows</Label>
<Letter>C</Letter>
</ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/NAME</Key>
<Value>@IMAGE_NAME@</Value>
</MetaData>
</InstallFrom>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>1</PartitionID>
</InstallTo>
</OSImage>
</ImageInstall>
<UserData>
<AcceptEula>true</AcceptEula>
<FullName>CST</FullName>
<Organization>CST</Organization>
</UserData>
</component>
</settings>
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<ComputerName>cst-win</ComputerName>
<TimeZone>China Standard Time</TimeZone>
</component>
</settings>
<settings pass="oobeSystem">
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<ProtectYourPC>3</ProtectYourPC>
<NetworkLocation>Work</NetworkLocation>
</OOBE>
<UserAccounts>
<AdministratorPassword>
<Value>@ADMIN_PASSWORD@</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
</UserAccounts>
<AutoLogon>
<Enabled>true</Enabled>
<Username>Administrator</Username>
<Password>
<Value>@ADMIN_PASSWORD@</Value>
<PlainText>true</PlainText>
</Password>
<LogonCount>1</LogonCount>
</AutoLogon>
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "foreach ($d in Get-PSDrive -PSProvider FileSystem) { $p = Join-Path $d.Root 'setup.ps1'; if (Test-Path $p) { &amp; $p; break } }"</CommandLine>
<Description>Enable OpenSSH</Description>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>
+28
View File
@@ -0,0 +1,28 @@
# Idempotent VM setup after install: test users, sshd access, console defaults.
# Users come from C:\Windows\Temp\cst-users.txt ("name<TAB>password", UTF-8), deleted afterwards.
$ErrorActionPreference = 'Stop'
$list = 'C:\Windows\Temp\cst-users.txt'
if (Test-Path $list) {
foreach ($line in Get-Content -Path $list -Encoding UTF8) {
$name, $pw = $line -split "`t", 2
if (-not $name) { continue }
$sec = ConvertTo-SecureString $pw -AsPlainText -Force
if (Get-LocalUser -Name $name -ErrorAction SilentlyContinue) {
Set-LocalUser -Name $name -Password $sec
} else {
New-LocalUser -Name $name -Password $sec -PasswordNeverExpires -AccountNeverExpires | Out-Null
}
foreach ($g in 'Users', 'OpenSSH Users') {
Add-LocalGroupMember -Group $g -Member $name -ErrorAction SilentlyContinue
}
"user ok: $name"
}
Remove-Item -Force $list
}
# the built-in rule only covers the Private profile; QEMU user networking is Public
Set-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -Profile Any -Enabled True
Set-SConfig -AutoLaunch $false
New-Item -ItemType Directory -Force C:\cst-test | Out-Null
"provision ok"
+51
View File
@@ -0,0 +1,51 @@
# Run cross-built test_*.exe inside the VM as the current user.
# run_tests.ps1 BUNDLE [FILTER]
# BUNDLE holds bin\ (exes, busybox.exe) and data\ (repo-relative fixtures).
# Each run copies data\ into a per-user work dir and uses it as the cwd.
param([Parameter(Mandatory = $true)][string]$Bundle, [string]$Filter = '*')
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
$OutputEncoding = [Console]::OutputEncoding
$work = Join-Path $env:TEMP ('cst-test-' + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Force $work | Out-Null
Copy-Item -Recurse -Force (Join-Path $Bundle 'data\*') $work
$env:PATH = (Join-Path $Bundle 'bin') + ';' + $env:PATH
$acp = [Text.Encoding]::Default.CodePage
"== user=$env:USERNAME acp=$acp work=$work"
$fail = 0
$pass = 0
foreach ($exe in Get-ChildItem (Join-Path $Bundle 'bin') -Filter "test_$Filter.exe" | Sort-Object Name) {
$name = $exe.BaseName
$out = Join-Path $work "$name.out"
$err = Join-Path $work "$name.err"
$sw = [Diagnostics.Stopwatch]::StartNew()
$p = Start-Process -FilePath $exe.FullName -WorkingDirectory $work -NoNewWindow -PassThru `
-RedirectStandardOutput $out -RedirectStandardError $err
$null = $p.Handle
if (-not $p.WaitForExit(300000)) {
& taskkill.exe /T /F /PID $p.Id | Out-Null
$code = 'timeout'
} else {
$p.WaitForExit()
$code = $p.ExitCode
}
$secs = [math]::Round($sw.Elapsed.TotalSeconds, 1)
if ($code -eq 0) {
$pass++
"PASS $name (${secs}s)"
} else {
$fail++
"FAIL $name (exit $code, ${secs}s)"
foreach ($f in $out, $err) {
if (Test-Path $f) {
Get-Content -Path $f -Encoding UTF8 -Tail 25 | ForEach-Object { " $_" }
}
}
}
}
"== $pass passed, $fail failed"
Set-Location $env:TEMP
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
exit $fail
+28
View File
@@ -0,0 +1,28 @@
Start-Transcript -Path C:\setup.log -Append
$ErrorActionPreference = 'Continue'
if (-not (Get-Service sshd -ErrorAction SilentlyContinue)) {
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
}
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
$keys = 'C:\ProgramData\ssh\administrators_authorized_keys'
Get-Content (Join-Path $PSScriptRoot 'vm_key.pub') | Set-Content -Path $keys -Encoding ascii
icacls $keys /inheritance:r /grant 'Administrators:F' /grant 'SYSTEM:F'
New-ItemProperty -Path HKLM:\SOFTWARE\OpenSSH -Name DefaultShell -PropertyType String -Force `
-Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
if (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue) {
# the built-in rule only covers the Private profile; QEMU user networking is Public
Set-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -Profile Any -Enabled True
} else {
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' `
-Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -Profile Any
}
powercfg /change monitor-timeout-ac 0
powercfg /change standby-timeout-ac 0
New-Item -Path C:\setup.done -ItemType File -Force
Stop-Transcript
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Type text into the VM console via QMP.
type.py QMP_SOCKET TEXT (\\n = Enter)
type.py QMP_SOCKET @FILE the file's first line, then Enter (secrets stay
off the command line)
type.py QMP_SOCKET --cad Ctrl+Alt+Del
"""
import json
import os
import socket
import sys
import time
SHIFTED = {
'!': '1', '@': '2', '#': '3', '$': '4', '%': '5', '^': '6', '&': '7',
'*': '8', '(': '9', ')': '0', '_': 'minus', '+': 'equal', '{': 'bracket_left',
'}': 'bracket_right', '|': 'backslash', ':': 'semicolon', '"': 'apostrophe',
'<': 'comma', '>': 'dot', '?': 'slash', '~': 'grave_accent',
}
PLAIN = {
' ': 'spc', '\n': 'ret', '\t': 'tab', '-': 'minus', '=': 'equal',
'[': 'bracket_left', ']': 'bracket_right', '\\': 'backslash', ';': 'semicolon',
"'": 'apostrophe', ',': 'comma', '.': 'dot', '/': 'slash', '`': 'grave_accent',
}
def keys_for(ch):
if ch.isascii() and ch.isalpha():
return (['shift'] if ch.isupper() else []) + [ch.lower()]
if ch.isdigit():
return [ch]
if ch in PLAIN:
return [PLAIN[ch]]
if ch in SHIFTED:
return ['shift', SHIFTED[ch]]
raise SystemExit(f'unsupported char {ch!r}')
def main():
arg = sys.argv[2]
if arg == '--cad':
combos = [['ctrl', 'alt', 'delete']]
else:
if arg.startswith('@'):
with open(arg[1:], encoding='utf-8') as fp:
text = fp.readline().rstrip('\r\n') + '\n'
else:
text = arg.encode().decode('unicode_escape')
combos = [keys_for(ch) for ch in text]
s = socket.socket(socket.AF_UNIX)
s.connect(sys.argv[1])
f = s.makefile('rw')
f.readline()
f.write(json.dumps({'execute': 'qmp_capabilities'}) + '\n')
f.flush()
f.readline()
for combo in combos:
keys = [{'type': 'qcode', 'data': k} for k in combo]
f.write(json.dumps({'execute': 'send-key', 'arguments': {'keys': keys}}) + '\n')
f.flush()
f.readline()
time.sleep(0.03)
main()
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env bash
# Headless Windows Server 2025 Core test VMs (QEMU/KVM).
#
# vm.sh create download the evaluation ISO, install base.qcow2
# vm.sh variants en.qcow2 / zh.qcow2 overlays on top of base.qcow2
# (zh: zh-CN system locale, ANSI code page 936)
# vm.sh start|stop|status|wait VARIANT
# vm.sh ssh VARIANT [USER] [command...] USER: Administrator (default), tester, 测试
# vm.sh put VARIANT SRC... DEST scp as Administrator
# vm.sh ps VARIANT [USER] FILE.ps1 [args...] run a script (uploaded with a UTF-8 BOM)
# vm.sh shot VARIANT [out.png] console screenshot
# vm.sh type VARIANT TEXT type into the console (\n = Enter)
# vm.sh login VARIANT unlock the console as Administrator
#
# State (ISO, disks, keys, passwords) lives in $CST_WINVM_DIR, default ~/winvm.
# Keys and passwords are created there with mode 0600 and never printed.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
D="${CST_WINVM_DIR:-$HOME/winvm}"
ISO_URL="https://aka.ms/WinServ2025iso-enus"
IMAGE_NAME="Windows Server 2025 SERVERSTANDARDCORE"
USERS=(tester 测试)
if ! command -v qemu-system-x86_64 >/dev/null || ! command -v sshpass >/dev/null ||
! command -v socat >/dev/null || ! command -v xorriso >/dev/null; then
if command -v nix-shell >/dev/null && [[ -z "${CST_WINVM_NIX:-}" ]]; then
export CST_WINVM_NIX=1
exec nix-shell -p qemu sshpass socat xorriso imagemagick python3 \
--run "$(printf '%q ' "$0" "$@")"
fi
echo "need qemu-system-x86_64, sshpass, socat, xorriso" >&2
exit 1
fi
die() { echo "vm.sh: $*" >&2; exit 1; }
port_of() {
case "$1" in
base) echo 2221 ;;
en) echo 2222 ;;
zh) echo 2223 ;;
*) die "unknown variant '$1' (base, en, zh)" ;;
esac
}
pw_file() {
case "$1" in
Administrator) echo "$D/admin-password" ;;
tester) echo "$D/tester-password" ;;
测试) echo "$D/ceshi-password" ;;
*) die "unknown user '$1'" ;;
esac
}
new_password() { # prefix -> file, mixed classes for Windows complexity rules
local f="$2"
[[ -s "$f" ]] && return 0
(umask 077; printf '%s%s9a' "$1" "$(head -c 256 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 16)" >"$f")
}
running() {
local pid
pid="$(cat "$D/$1.pid" 2>/dev/null)" || return 1
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
ssh_opts() {
echo -p "$(port_of "$1")" -o UserKnownHostsFile="$D/known_hosts" \
-o StrictHostKeyChecking=accept-new -o LogLevel=ERROR -o ConnectTimeout=10
}
vm_ssh() { # variant user command...
local v="$1" u="$2"
shift 2
# shellcheck disable=SC2046
if [[ "$u" == Administrator ]]; then
ssh -i "$D/vm_key" -o BatchMode=yes $(ssh_opts "$v") "Administrator@127.0.0.1" "$@"
else
SSHPASS="$(cat "$(pw_file "$u")")" sshpass -e ssh -o PubkeyAuthentication=no \
-o PreferredAuthentications=password $(ssh_opts "$v") "$u@127.0.0.1" "$@"
fi
}
vm_put() { # variant src... dest
local v="$1"
shift
local dest="${!#}"
local srcs=("${@:1:$#-1}")
# shellcheck disable=SC2046
scp -q -r -i "$D/vm_key" -o BatchMode=yes $(ssh_opts "$v" | sed 's/^-p/-P/') \
"${srcs[@]}" "Administrator@127.0.0.1:$dest"
}
qemu_start() { # variant disk [extra args...]
local v="$1" disk="$2"
shift 2
running "$v" && return 0
[[ -f "$disk" ]] || die "missing $disk"
qemu-system-x86_64 \
-name "cst-win-$v" -machine q35,accel=kvm \
-cpu host,hv_relaxed,hv_spinlocks=0x1fff,hv_vapic,hv_time \
-smp 4 -m 6G -rtc base=localtime \
-device ahci,id=ahci \
-drive file="$disk",if=none,id=d0,cache=writeback,discard=unmap \
-device ide-hd,drive=d0,bus=ahci.0 \
-netdev user,id=n0,hostfwd=tcp:127.0.0.1:"$(port_of "$v")"-:22 \
-device e1000e,netdev=n0 \
-device qemu-xhci,id=xhci -device usb-tablet,bus=xhci.0 \
-display none -qmp unix:"$D/$v.qmp",server,nowait \
-pidfile "$D/$v.pid" -daemonize "$@"
}
disk_of() {
case "$1" in
base) echo "$D/base.qcow2" ;;
*) echo "$D/$1.qcow2" ;;
esac
}
vm_wait() { # variant [timeout_s]
local v="$1" t="${2:-900}" i
for ((i = 0; i < t; i += 5)); do
if vm_ssh "$v" Administrator 'Test-Path C:\setup.done' 2>/dev/null | grep -q True; then
return 0
fi
running "$v" || die "$v is not running"
sleep 5
done
die "$v: no SSH after ${t}s"
}
vm_stop() {
local v="$1" i
running "$v" || return 0
vm_ssh "$v" Administrator 'Stop-Computer -Force' >/dev/null 2>&1 || true
for ((i = 0; i < 120; i++)); do
running "$v" || { rm -f "$D/$v.pid" "$D/$v.qmp"; return 0; }
sleep 1
done
printf '%s\n' '{"execute":"qmp_capabilities"}' '{"execute":"quit"}' |
socat - UNIX-CONNECT:"$D/$v.qmp" >/dev/null || true
rm -f "$D/$v.pid" "$D/$v.qmp"
}
with_bom() { # file -> temp copy with a UTF-8 BOM (Windows PowerShell 5.1 reads ANSI otherwise)
local out
out="$(mktemp --suffix=.ps1)"
printf '\xef\xbb\xbf' >"$out"
sed '1s/^\xef\xbb\xbf//' "$1" >>"$out"
echo "$out"
}
vm_ps() { # variant user file args...
local v="$1" u="$2" f="$3"
shift 3
local tmp name
tmp="$(with_bom "$f")"
name="cst-$(basename "$f" .ps1)-$$.ps1"
vm_put "$v" "$tmp" "C:/Windows/Temp/$name"
rm -f "$tmp"
vm_ssh "$v" Administrator "icacls C:\\Windows\\Temp\\$name /grant 'Users:RX' | Out-Null" >/dev/null
local rc=0
vm_ssh "$v" "$u" "powershell -NoProfile -ExecutionPolicy Bypass -File C:\\Windows\\Temp\\$name $*" || rc=$?
vm_ssh "$v" Administrator "Remove-Item -Force C:\\Windows\\Temp\\$name" >/dev/null 2>&1 || true
return $rc
}
provision() { # variant: users, sshd, console; idempotent
local v="$1" u tmp
tmp="$(mktemp)"
chmod 600 "$tmp"
for u in "${USERS[@]}"; do
new_password "Tst!" "$(pw_file "$u")"
printf '%s\t%s\n' "$u" "$(cat "$(pw_file "$u")")" >>"$tmp"
done
vm_put "$v" "$tmp" "C:/Windows/Temp/cst-users.txt"
rm -f "$tmp"
vm_ps "$v" Administrator "$HERE/provision.ps1"
}
cmd_create() {
mkdir -p "$D"
chmod 700 "$D"
[[ -f "$D/base.qcow2" ]] && die "$D/base.qcow2 exists"
if [[ ! -s "$D/server2025.iso" ]]; then
echo "==> downloading the Windows Server 2025 evaluation ISO (~8 GB)"
curl -fL -C - -o "$D/server2025.iso" "$ISO_URL"
fi
[[ -f "$D/vm_key" ]] || ssh-keygen -q -t ed25519 -N "" -C cst-winvm -f "$D/vm_key"
new_password "Cst!" "$D/admin-password"
local u
u="$(mktemp -d)"
chmod 700 "$u"
cp "$HERE/setup.ps1" "$D/vm_key.pub" "$u/"
(umask 077
sed -e "s|@IMAGE_NAME@|$IMAGE_NAME|" -e "s|@ADMIN_PASSWORD@|$(cat "$D/admin-password")|g" \
"$HERE/autounattend.xml.in" >"$u/autounattend.xml"
xorriso -as mkisofs -quiet -J -r -V UNATTEND -o "$D/unattend.iso" "$u/" 2>/dev/null)
rm -rf "$u"
qemu-img create -q -f qcow2 "$D/base.qcow2" 60G
echo "==> unattended install (20-30 minutes)"
qemu_start base "$D/base.qcow2" \
-drive file="$D/server2025.iso",media=cdrom,if=none,id=cd0,readonly=on \
-device ide-cd,drive=cd0,bus=ahci.1 \
-drive file="$D/unattend.iso",media=cdrom,if=none,id=cd1,readonly=on \
-device ide-cd,drive=cd1,bus=ahci.2 \
-boot order=c,once=d
vm_wait base 3600
provision base
vm_stop base
echo "OK: $D/base.qcow2; next: vm.sh variants"
}
cmd_variants() {
[[ -f "$D/base.qcow2" ]] || die "no base.qcow2; run vm.sh create"
running base && die "stop the base VM first"
local v
for v in en zh; do
running "$v" && vm_stop "$v"
rm -f "$D/$v.qcow2"
qemu-img create -q -f qcow2 -b "$D/base.qcow2" -F qcow2 "$D/$v.qcow2"
done
qemu_start zh "$D/zh.qcow2"
vm_wait zh
vm_ssh zh Administrator 'Set-WinSystemLocale -SystemLocale zh-CN; Set-Culture zh-CN; Set-WinHomeLocation -GeoId 45; Restart-Computer -Force' || true
sleep 20
vm_wait zh
echo "zh ANSI code page: $(vm_ssh zh Administrator '[Text.Encoding]::Default.CodePage')"
vm_stop zh
echo "OK: en.qcow2 zh.qcow2"
}
cmd="${1:-}"
shift || true
case "$cmd" in
create) cmd_create ;;
variants) cmd_variants ;;
provision) provision "${1:?variant}" ;;
start)
qemu_start "${1:?variant}" "$(disk_of "$1")"
vm_wait "$1"
;;
stop) vm_stop "${1:?variant}" ;;
status) running "${1:?variant}" && echo running || echo stopped ;;
wait) vm_wait "${1:?variant}" "${2:-900}" ;;
ssh)
v="${1:?variant}"
shift
u=Administrator
if [[ $# -gt 0 ]] && [[ "$1" == Administrator || "$1" == tester || "$1" == 测试 ]]; then
u="$1"
shift
fi
vm_ssh "$v" "$u" "$@"
;;
put) vm_put "$@" ;;
ps)
v="${1:?variant}"
shift
u=Administrator
if [[ "$1" == Administrator || "$1" == tester || "$1" == 测试 ]]; then
u="$1"
shift
fi
vm_ps "$v" "$u" "$@"
;;
shot)
v="${1:?variant}"
out="${2:-$D/$v.png}"
printf '%s\n' '{"execute":"qmp_capabilities"}' \
'{"execute":"screendump","arguments":{"filename":"'"$D/$v.ppm"'"}}' |
socat - UNIX-CONNECT:"$D/$v.qmp" >/dev/null
sleep 1
magick "$D/$v.ppm" -resize '1024x1024>' "$out"
rm -f "$D/$v.ppm"
echo "$out"
;;
type) python3 "$HERE/type.py" "$D/${1:?variant}.qmp" "${2:?text}" ;;
login)
python3 "$HERE/type.py" "$D/${1:?variant}.qmp" --cad
sleep 3
python3 "$HERE/type.py" "$D/$1.qmp" "@$D/admin-password"
;;
*) sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
esac