From f208e07d7245c7d6b5eb4383190e2c18040a2c5a Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 21 Sep 2026 22:01:11 -0700 Subject: [PATCH] Fix first-logon provisioning never starting - Bound every installer wait with WaitForExit and a timeout instead of Start-Process -Wait, which in Windows PowerShell also waits for any process the installer leaves running and can block forever. This covers the VirtIO step in the answer files and the guest tools, Mesh agent and Winget installs in the scripts. - specialize pass (SYSTEM) sets ConsentPromptBehaviorAdmin=0 so nothing at first logon can stall on a UAC prompt. - Step 4 now logs to C:\ProgramData\OOBE\firstlogon.log, saves the script to C:\ProgramData\OOBE and starts it elevated in its own window. - Scripts start their transcript before the admin check, so an early stop still leaves a log. Co-Authored-By: Claude Opus 5 --- oobe-desktop.ps1 | 29 ++++++++++++++++-------- oobe-vms.ps1 | 39 +++++++++++++++++++++++--------- unattend/desktop-PROMPT-DISK.xml | 31 +++++++++++++++++++++---- unattend/vms-WIPE-DISK.xml | 31 +++++++++++++++++++++---- 4 files changed, 102 insertions(+), 28 deletions(-) diff --git a/oobe-desktop.ps1 b/oobe-desktop.ps1 index 9d76589..d8dcd39 100644 --- a/oobe-desktop.ps1 +++ b/oobe-desktop.ps1 @@ -3,12 +3,6 @@ # irm https://url.isworking.fyi/oobe-desktop | iex # Log: C:\ProgramData\OOBE\oobe-desktop.log -# Check if running with elevated privileges -if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Host "Please run this script as an administrator." - Exit 1 -} - $MeshAgentUrl = "https://rmm.iamchrisama.com/meshagents?id=4&meshid=zxl@U2zM95zh9ZNqah@9mUEjCJ3ptGOE6s5cjsGniacVU1fjRXtVKCKlKJN4aJQW&installflags=0" $ErrorActionPreference = 'Continue' @@ -21,6 +15,13 @@ $WorkDir = Join-Path $env:ProgramData 'OOBE' New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null Start-Transcript -Path (Join-Path $WorkDir 'oobe-desktop.log') -Append | Out-Null +# Check if running with elevated privileges (after the transcript starts, so this still leaves a log) +if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Warning "Not running as administrator, stopping. Rerun from an elevated PowerShell." + Stop-Transcript | Out-Null + Exit 1 +} + function Save-Download($Uri, $OutFile) { for ($Attempt = 1; $Attempt -le 3; $Attempt++) { try { @@ -110,7 +111,10 @@ if (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue) { $MeshInstaller = Join-Path $WorkDir 'meshagent.exe' if (Save-Download $MeshAgentUrl $MeshInstaller) { # -fullinstall installs and starts the service without showing the install dialog - Start-Process -FilePath $MeshInstaller -ArgumentList '-fullinstall' -WindowStyle Hidden -Wait + $Process = Start-Process -FilePath $MeshInstaller -ArgumentList '-fullinstall' -WindowStyle Hidden -PassThru + if (-not $Process.WaitForExit(300000)) { + Write-Warning "Mesh Agent installer still running after 5 minutes, continuing." + } } if (-not (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue)) { Write-Warning "Mesh Agent service was not found after install." @@ -140,10 +144,17 @@ if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyCon } # Install Winget (LTSC ships without the Store / App Installer) -# Run in a child process: the installer script calls exit, which would end this script too +# Run in a child process (the installer script calls exit, which would end this script too), +# with a time limit so it can't hold up the restart. Its output goes to winget-install*.log. $WingetInstaller = Join-Path $WorkDir 'winget-install.ps1' if (Save-Download 'https://github.com/asheroto/winget-install/releases/latest/download/winget-install.ps1' $WingetInstaller) { - & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $WingetInstaller -Force + $Process = Start-Process powershell.exe -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $WingetInstaller, '-Force' -PassThru ` + -RedirectStandardOutput (Join-Path $WorkDir 'winget-install.log') -RedirectStandardError (Join-Path $WorkDir 'winget-install-errors.log') + if ($Process.WaitForExit(900000)) { + Write-Output "Winget installer exited with code $($Process.ExitCode)" + } else { + Write-Warning "Winget installer still running after 15 minutes, continuing." + } } # Restart to finish OpenSSH and driver installation. diff --git a/oobe-vms.ps1 b/oobe-vms.ps1 index 7d2ca66..a1d275e 100644 --- a/oobe-vms.ps1 +++ b/oobe-vms.ps1 @@ -3,12 +3,6 @@ # irm https://url.isworking.fyi/oobe-vms | iex # Log: C:\ProgramData\OOBE\oobe-vms.log -# Check if running with elevated privileges -if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Host "Please run this script as an administrator." - Exit 1 -} - $MeshAgentUrl = "https://rmm.iamchrisama.com/meshagents?id=4&meshid=zxl@U2zM95zh9ZNqah@9mUEjCJ3ptGOE6s5cjsGniacVU1fjRXtVKCKlKJN4aJQW&installflags=0" $ErrorActionPreference = 'Continue' @@ -21,6 +15,13 @@ $WorkDir = Join-Path $env:ProgramData 'OOBE' New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null Start-Transcript -Path (Join-Path $WorkDir 'oobe-vms.log') -Append | Out-Null +# Check if running with elevated privileges (after the transcript starts, so this still leaves a log) +if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + Write-Warning "Not running as administrator, stopping. Rerun from an elevated PowerShell." + Stop-Transcript | Out-Null + Exit 1 +} + function Save-Download($Uri, $OutFile) { for ($Attempt = 1; $Attempt -le 3; $Attempt++) { try { @@ -75,8 +76,14 @@ if (Get-Service -Name 'QEMU-GA' -ErrorAction SilentlyContinue) { ForEach-Object { $Store.Add($_) } $Store.Close() - $Process = Start-Process -FilePath $GuestTools -ArgumentList '/install', '/quiet', '/norestart' -Wait -PassThru - Write-Output "VirtIO guest tools installer exited with code $($Process.ExitCode) (0 = success, 3010 = reboot required)" + # Wait on the installer only, with a time limit: Start-Process -Wait also waits for anything + # the installer leaves running (agents, services), which can block forever + $Process = Start-Process -FilePath $GuestTools -ArgumentList '/install', '/quiet', '/norestart' -PassThru + if ($Process.WaitForExit(900000)) { + Write-Output "VirtIO guest tools installer exited with code $($Process.ExitCode) (0 = success, 3010 = reboot required)" + } else { + Write-Warning "VirtIO guest tools installer still running after 15 minutes, continuing." + } } } @@ -176,7 +183,10 @@ if (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue) { $MeshInstaller = Join-Path $WorkDir 'meshagent.exe' if (Save-Download $MeshAgentUrl $MeshInstaller) { # -fullinstall installs and starts the service without showing the install dialog - Start-Process -FilePath $MeshInstaller -ArgumentList '-fullinstall' -WindowStyle Hidden -Wait + $Process = Start-Process -FilePath $MeshInstaller -ArgumentList '-fullinstall' -WindowStyle Hidden -PassThru + if (-not $Process.WaitForExit(300000)) { + Write-Warning "Mesh Agent installer still running after 5 minutes, continuing." + } } if (-not (Get-Service -Name 'Mesh Agent' -ErrorAction SilentlyContinue)) { Write-Warning "Mesh Agent service was not found after install." @@ -206,10 +216,17 @@ if (!(Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyCon } # Install Winget (LTSC ships without the Store / App Installer) -# Run in a child process: the installer script calls exit, which would end this script too +# Run in a child process (the installer script calls exit, which would end this script too), +# with a time limit so it can't hold up the restart. Its output goes to winget-install*.log. $WingetInstaller = Join-Path $WorkDir 'winget-install.ps1' if (Save-Download 'https://github.com/asheroto/winget-install/releases/latest/download/winget-install.ps1' $WingetInstaller) { - & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $WingetInstaller -Force + $Process = Start-Process powershell.exe -ArgumentList '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $WingetInstaller, '-Force' -PassThru ` + -RedirectStandardOutput (Join-Path $WorkDir 'winget-install.log') -RedirectStandardError (Join-Path $WorkDir 'winget-install-errors.log') + if ($Process.WaitForExit(900000)) { + Write-Output "Winget installer exited with code $($Process.ExitCode)" + } else { + Write-Warning "Winget installer still running after 15 minutes, continuing." + } } # Restart to finish driver, guest tools and OpenSSH installation. diff --git a/unattend/desktop-PROMPT-DISK.xml b/unattend/desktop-PROMPT-DISK.xml index 599ee7a..6c645db 100644 --- a/unattend/desktop-PROMPT-DISK.xml +++ b/unattend/desktop-PROMPT-DISK.xml @@ -141,6 +141,25 @@ ======================================================================= --> + + + + + 1 + Elevate admins without a UAC prompt + reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f + + + + + disc moves around post-install. Waits at most 15 minutes, so a stuck + installer can never block the next step. --> 3 Install VirtIO guest tools - powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-Volume | Where-Object DriveLetter | ForEach-Object { $exe = $_.DriveLetter + ':\virtio-win-guest-tools.exe'; if (Test-Path $exe) { Start-Process $exe -ArgumentList '/install','/quiet','/norestart' -Wait } }" + powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-Volume | Where-Object DriveLetter | ForEach-Object { $exe = $_.DriveLetter + ':\virtio-win-guest-tools.exe'; if (Test-Path $exe) { $p = Start-Process $exe -ArgumentList '/install','/quiet','/norestart' -PassThru; [void]$p.WaitForExit(900000) } }" false 4 Run oobe-desktop.ps1 (url.isworking.fyi, else ISO copy) - powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $s = $null; for ($i = 1; $i -le 10; $i++) { try { $s = Invoke-RestMethod 'https://url.isworking.fyi/oobe-desktop'; break } catch { Start-Sleep -Seconds 15 } }; if (-not $s) { $f = Get-PSDrive -PSProvider FileSystem | ForEach-Object { Join-Path $_.Root 'oobe-desktop.ps1' } | Where-Object { Test-Path $_ } | Select-Object -First 1; if ($f) { $s = Get-Content -Raw $f } }; if ($s) { Invoke-Expression $s } else { Write-Warning 'oobe-desktop.ps1 not found online or on any drive'; Start-Sleep -Seconds 60 }" + powershell -NoProfile -ExecutionPolicy Bypass -Command "$d = Join-Path $env:ProgramData 'OOBE'; New-Item -ItemType Directory -Force $d | Out-Null; Start-Transcript (Join-Path $d 'firstlogon.log') -Append; [Net.ServicePointManager]::SecurityProtocol = 'Tls12'; $s = $null; for ($i = 1; $i -le 10; $i++) { try { $s = irm 'https://url.isworking.fyi/oobe-desktop' -UseBasicParsing; 'Downloaded'; break } catch { 'Attempt ' + $i + ' failed: ' + $_; sleep 15 } }; if (-not $s) { $f = Get-PSDrive -PSProvider FileSystem | % { Join-Path $_.Root 'oobe-desktop.ps1' } | ? { Test-Path $_ } | select -First 1; if ($f) { 'Using ' + $f; $s = Get-Content -Raw $f } }; if ($s) { $p = Join-Path $d 'oobe-desktop.ps1'; Set-Content $p $s; Start-Process powershell -Verb RunAs -ArgumentList ('-NoProfile -ExecutionPolicy Bypass -File ' + $p); 'Started ' + $p } else { Write-Warning 'oobe-desktop.ps1 not found online or on any drive'; sleep 60 }" false diff --git a/unattend/vms-WIPE-DISK.xml b/unattend/vms-WIPE-DISK.xml index 7c11d1d..3ca9a63 100644 --- a/unattend/vms-WIPE-DISK.xml +++ b/unattend/vms-WIPE-DISK.xml @@ -188,6 +188,25 @@ ======================================================================= --> + + + + + 1 + Elevate admins without a UAC prompt + reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f + + + + + disc moves around post-install. Waits at most 15 minutes, so a stuck + installer can never block the next step. --> 3 Install VirtIO guest tools - powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-Volume | Where-Object DriveLetter | ForEach-Object { $exe = $_.DriveLetter + ':\virtio-win-guest-tools.exe'; if (Test-Path $exe) { Start-Process $exe -ArgumentList '/install','/quiet','/norestart' -Wait } }" + powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-Volume | Where-Object DriveLetter | ForEach-Object { $exe = $_.DriveLetter + ':\virtio-win-guest-tools.exe'; if (Test-Path $exe) { $p = Start-Process $exe -ArgumentList '/install','/quiet','/norestart' -PassThru; [void]$p.WaitForExit(900000) } }" false 4 Run oobe-vms.ps1 (url.isworking.fyi, else ISO copy) - powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $s = $null; for ($i = 1; $i -le 10; $i++) { try { $s = Invoke-RestMethod 'https://url.isworking.fyi/oobe-vms'; break } catch { Start-Sleep -Seconds 15 } }; if (-not $s) { $f = Get-PSDrive -PSProvider FileSystem | ForEach-Object { Join-Path $_.Root 'oobe-vms.ps1' } | Where-Object { Test-Path $_ } | Select-Object -First 1; if ($f) { $s = Get-Content -Raw $f } }; if ($s) { Invoke-Expression $s } else { Write-Warning 'oobe-vms.ps1 not found online or on any drive'; Start-Sleep -Seconds 60 }" + powershell -NoProfile -ExecutionPolicy Bypass -Command "$d = Join-Path $env:ProgramData 'OOBE'; New-Item -ItemType Directory -Force $d | Out-Null; Start-Transcript (Join-Path $d 'firstlogon.log') -Append; [Net.ServicePointManager]::SecurityProtocol = 'Tls12'; $s = $null; for ($i = 1; $i -le 10; $i++) { try { $s = irm 'https://url.isworking.fyi/oobe-vms' -UseBasicParsing; 'Downloaded'; break } catch { 'Attempt ' + $i + ' failed: ' + $_; sleep 15 } }; if (-not $s) { $f = Get-PSDrive -PSProvider FileSystem | % { Join-Path $_.Root 'oobe-vms.ps1' } | ? { Test-Path $_ } | select -First 1; if ($f) { 'Using ' + $f; $s = Get-Content -Raw $f } }; if ($s) { $p = Join-Path $d 'oobe-vms.ps1'; Set-Content $p $s; Start-Process powershell -Verb RunAs -ArgumentList ('-NoProfile -ExecutionPolicy Bypass -File ' + $p); 'Started ' + $p } else { Write-Warning 'oobe-vms.ps1 not found online or on any drive'; sleep 60 }" false