From 72af2763090b8589d341968b5f86a460bd3f2e5f Mon Sep 17 00:00:00 2001 From: Guofang Date: Mon, 26 Oct 2026 09:27:52 +0800 Subject: [PATCH] Add an update-status tool and copy numbered scripts to Desktop\Tools 80-Get-UpdateStatus-Admin.ps1 prints whether the Windows 11 guest is upgrading or downloading updates (policy, pending markers, services, processes, BITS jobs, update events, network use); the .bat launcher elevates and keeps the window open. The seed ISO and Configure-Windows.ps1 now take every NN-*.bat and NN-*.ps1, and the first-logon copy goes to Desktop\Tools instead of Desktop\Installers. Co-Authored-By: Claude Sonnet 5.5 --- run-windows11.py | 3 +- .../windows11/80-Get-UpdateStatus-Admin.bat | 14 +++ .../windows11/80-Get-UpdateStatus-Admin.ps1 | 104 ++++++++++++++++++ seediso/windows11/Configure-Windows.ps1 | 6 +- seediso/windows11/build-seed.sh | 5 +- 5 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 seediso/windows11/80-Get-UpdateStatus-Admin.bat create mode 100644 seediso/windows11/80-Get-UpdateStatus-Admin.ps1 diff --git a/run-windows11.py b/run-windows11.py index ed06dd9..7dd52be 100755 --- a/run-windows11.py +++ b/run-windows11.py @@ -287,7 +287,8 @@ def build_seed_iso(image_downloader, force=False): """Build the seed ISO, unless it is newer than all of its sources and not forced.""" seed_dir = script_dir / "seediso" / "windows11" seed_sources = [seed_dir / "Autounattend.xml", seed_dir / "Configure-Windows.ps1", - *sorted(seed_dir.glob("[0-9][0-9]-*.bat"))] + *sorted(seed_dir.glob("[0-9][0-9]-*.bat")), + *sorted(seed_dir.glob("[0-9][0-9]-*.ps1"))] # build-seed.sh embeds the administrator SSH public key. ssh_key = os.environ.get("SSH_PUBLIC_KEY_FILE") or next( diff --git a/seediso/windows11/80-Get-UpdateStatus-Admin.bat b/seediso/windows11/80-Get-UpdateStatus-Admin.bat new file mode 100644 index 0000000..14894b5 --- /dev/null +++ b/seediso/windows11/80-Get-UpdateStatus-Admin.bat @@ -0,0 +1,14 @@ +@echo off +setlocal +title Windows update status + +fltmc >nul 2>&1 +if errorlevel 1 ( + echo Requesting administrator rights... + powershell.exe -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" + exit /b +) + +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp080-Get-UpdateStatus-Admin.ps1" +echo. +pause diff --git a/seediso/windows11/80-Get-UpdateStatus-Admin.ps1 b/seediso/windows11/80-Get-UpdateStatus-Admin.ps1 new file mode 100644 index 0000000..e2da015 --- /dev/null +++ b/seediso/windows11/80-Get-UpdateStatus-Admin.ps1 @@ -0,0 +1,104 @@ +# Prints whether the guest is upgrading or downloading updates. +# Read-only. Run as Administrator for complete results (BITS jobs of all users, +# command lines of system processes); a standard user gets partial output. +param( + [int]$EventCount = 8 +) + +$ErrorActionPreference = 'Continue' +$ProgressPreference = 'SilentlyContinue' + +function Write-Section([string]$Title) { + Write-Host '' + Write-Host "== $Title ==" +} + +function Out-Text($Value) { + $text = ($Value | Out-String).TrimEnd() + if ($text) { Write-Host $text } else { Write-Host '(none)' } +} + +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$isAdmin = [Security.Principal.WindowsPrincipal]::new($identity).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) + +Write-Section 'Host' +Write-Host ('Time: {0}' -f (Get-Date)) +Write-Host ('Admin: {0}' -f $isAdmin) +if (-not $isAdmin) { + Write-Host 'Warning: not elevated, BITS jobs and process command lines may be incomplete.' +} + +Write-Section 'Operating system' +Out-Text (Get-CimInstance Win32_OperatingSystem | + Select-Object Caption, Version, BuildNumber, LastBootUpTime | Format-List) + +Write-Section 'Update policy' +$wuPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' +$props = 'NoAutoUpdate', 'NoAutoRebootWithLoggedOnUsers', 'ExcludeWUDriversInQualityUpdate', + 'TargetReleaseVersion', 'TargetReleaseVersionInfo' +foreach ($item in @(@{ Path = "$wuPath\AU" }, @{ Path = $wuPath })) { + $values = Get-ItemProperty -Path $item.Path -ErrorAction SilentlyContinue + foreach ($name in $props) { + if ($values -and $values.PSObject.Properties[$name]) { + Write-Host ('{0,-34} {1}' -f $name, $values.$name) + } + } +} + +Write-Section 'Upgrade / pending-reboot markers' +$markers = [ordered]@{ + 'C:\$WINDOWS.~BT (upgrade staging)' = Test-Path -LiteralPath 'C:\$WINDOWS.~BT' + 'C:\Windows\WinSxS\pending.xml' = Test-Path -LiteralPath 'C:\Windows\WinSxS\pending.xml' + 'WindowsUpdate RebootRequired' = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' + 'Component Based Servicing RebootPending' = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' +} +foreach ($key in $markers.Keys) { + Write-Host ('{0,-42} {1}' -f $key, $markers[$key]) +} + +Write-Section 'Update services' +Out-Text (Get-Service wuauserv, UsoSvc, TrustedInstaller, bits, WaaSMedicSvc, DoSvc ` + -ErrorAction SilentlyContinue | + Select-Object Name, Status, StartType | Format-Table -AutoSize) + +Write-Section 'Update scheduled tasks' +Out-Text (Get-ScheduledTask ` + -TaskPath '\Microsoft\Windows\UpdateOrchestrator\*', '\Microsoft\Windows\WindowsUpdate\*' ` + -ErrorAction SilentlyContinue | + Select-Object TaskName, State | Format-Table -AutoSize) + +Write-Section 'Update-related processes' +$pattern = 'setup|TiWorker|TrustedInstaller|UsoClient|MoSetup|SetupHost|SetupPrep|MusNotification|wusa|dism|EdgeUpdate|Installer' +Out-Text (Get-CimInstance Win32_Process | + Where-Object { $_.Name -match $pattern -or $_.CommandLine -match 'wuauserv|UsoSvc|DoSvc' } | + Select-Object ProcessId, Name, CreationDate, CommandLine | Format-List) + +Write-Section 'BITS download jobs' +Out-Text (Get-BitsTransfer -AllUsers -ErrorAction SilentlyContinue | + Select-Object DisplayName, JobState, BytesTransferred, BytesTotal | Format-Table -AutoSize) + +Write-Section "Windows Update events (last $EventCount)" +Out-Text (Get-WinEvent -LogName 'Microsoft-Windows-WindowsUpdateClient/Operational' ` + -MaxEvents $EventCount -ErrorAction SilentlyContinue | + Select-Object TimeCreated, Id, Message | Format-List) + +Write-Section 'Established HTTP/HTTPS connections by process' +Out-Text (Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | + Where-Object { $_.RemotePort -in 80, 443 } | + Group-Object OwningProcess | + ForEach-Object { + $process = Get-Process -Id $_.Name -ErrorAction SilentlyContinue + [pscustomobject]@{ Pid = $_.Name; Process = $process.Name; Connections = $_.Count } + } | Format-Table -AutoSize) + +Write-Section 'Network receive rate (2 samples, 2 s apart)' +$counter = Get-Counter '\Network Interface(*)\Bytes Received/sec' ` + -SampleInterval 2 -MaxSamples 2 -ErrorAction SilentlyContinue +if ($counter) { + Out-Text ($counter[-1].CounterSamples | + Select-Object InstanceName, @{ n = 'KBps'; e = { [int]($_.CookedValue / 1024) } } | + Format-Table -AutoSize) +} else { + Write-Host '(performance counters unavailable)' +} diff --git a/seediso/windows11/Configure-Windows.ps1 b/seediso/windows11/Configure-Windows.ps1 index def2764..d980d79 100644 --- a/seediso/windows11/Configure-Windows.ps1 +++ b/seediso/windows11/Configure-Windows.ps1 @@ -144,12 +144,14 @@ function Disable-AutomaticUpdate { } function Copy-InstallerScripts { - $scripts = @(Get-ChildItem -Path $PSScriptRoot -Filter '*-Install-*.bat' -File) + # Numbered scripts (NN-*.bat / NN-*.ps1: installers and tools) go to Desktop\Tools. + $scripts = @(Get-ChildItem -Path $PSScriptRoot -File | + Where-Object { $_.Name -match '^\d\d-.*\.(bat|ps1)$' }) if ($scripts.Count -eq 0) { throw "No installer batch files were found next to $PSCommandPath." } - $target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Installers' + $target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Tools' New-Item -ItemType Directory -Path $target -Force | Out-Null Copy-Item -Path $scripts.FullName -Destination $target -Force } diff --git a/seediso/windows11/build-seed.sh b/seediso/windows11/build-seed.sh index 1b37b1c..6b7535d 100755 --- a/seediso/windows11/build-seed.sh +++ b/seediso/windows11/build-seed.sh @@ -43,9 +43,10 @@ if [[ -n "$ssh_key" ]]; then cp -- "$ssh_key" "$work_dir/authorized_keys" seed_files+=("$work_dir/authorized_keys") fi -# Numbered installer scripts (10-Install-Git-Admin.bat, 20-..., in install order). +# Numbered scripts: installers (10-Install-Git-Admin.bat, 20-..., in install order) +# and tools (80-Get-UpdateStatus-Admin.bat/.ps1); all land in Desktop\Tools. shopt -s nullglob -seed_files+=("$script_dir"/[0-9][0-9]-*.bat) +seed_files+=("$script_dir"/[0-9][0-9]-*.bat "$script_dir"/[0-9][0-9]-*.ps1) shopt -u nullglob mkdir -p "$(dirname -- "$output")"