From 508e55dac4a72dc7b25bfd4876c28ec69e24f8e8 Mon Sep 17 00:00:00 2001 From: jdickson289 Date: Sat, 9 May 2026 14:56:57 -0500 Subject: [PATCH 01/17] Add legacy driver and GlobalFlag IPU blocker detection to OSUpgrade Assessment script Adds two new pre-upgrade checks to Windows_OSUpgrade_Assessment_Validation.ps1: 1. Get-LegacyDriverBlockers: Scans the Services registry for known legacy VMware/ghost hardware drivers (vmmouse, vm3dmp, flpydisk, vmhgfs, vmrawdsk, vmusbmouse, vmvss, vmscsi, vmxnet) that are set to load (Start <= 3). These drivers cause IPU to fail with 0xC1900101-0x50016 when Windows Setup boots into SafeOS and encounters hardware unsupported by the Azure Hyper-V host. 2. Get-GlobalFlagStatus: Detects GlobalFlag enabled in Session Manager (HKLM\SYSTEM\CurrentControlSet\Control\Session Manager). When set, this forces Windows Setup into PageHeap/debug mode, throttling memory operations and causing IPU to time out and roll back. Both checks output [Failed] checklist items with inline remediation guidance: - Disable (not Uninstall) legacy devices in Device Manager before retrying IPU - Remove GlobalFlag via reg delete and reboot before retrying IPU Addresses scenario: lift-and-shift Azure VMs from VMware/on-premises environments. Related ADO: #35892966 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...indows_OSUpgrade_Assessment_Validation.ps1 | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/RunCommand/Windows/Windows_OSUpgrade_Assessment_Validation/Windows_OSUpgrade_Assessment_Validation.ps1 b/RunCommand/Windows/Windows_OSUpgrade_Assessment_Validation/Windows_OSUpgrade_Assessment_Validation.ps1 index 00618c3..570c5a7 100644 --- a/RunCommand/Windows/Windows_OSUpgrade_Assessment_Validation/Windows_OSUpgrade_Assessment_Validation.ps1 +++ b/RunCommand/Windows/Windows_OSUpgrade_Assessment_Validation/Windows_OSUpgrade_Assessment_Validation.ps1 @@ -33,6 +33,30 @@ Disclaimer: PS> .\Windows_OSUpgrade_Assessment_Validation.ps1 #> +# ---- Legacy driver and GlobalFlag detection ---------------------------------- +function Get-LegacyDriverBlockers { + # Known legacy VMware / ghost hardware drivers that cause 0xC1900101-0x50016 + $knownBlockers = @('vmmouse', 'vm3dmp', 'flpydisk', 'vmhgfs', 'vmrawdsk', 'vmusbmouse', 'vmvss', 'vmscsi', 'vmxnet') + $found = @() + foreach ($name in $knownBlockers) { + $svcPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$name" + if (Test-Path $svcPath) { + $start = (Get-ItemProperty $svcPath -ErrorAction SilentlyContinue).Start + # Start values: 0=Boot, 1=System, 2=Auto, 3=Manual, 4=Disabled + if ($null -ne $start -and $start -le 3) { + $found += $name + } + } + } + return $found +} + +function Get-GlobalFlagStatus { + $smPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" + $gf = (Get-ItemProperty $smPath -Name GlobalFlag -ErrorAction SilentlyContinue).GlobalFlag + return ($null -ne $gf -and $gf -ne 0) +} + # ---- Safety checks ----------------------------------------------------------- function Assert-Admin { $isAdmin = ([Security.Principal.WindowsPrincipal] ` @@ -202,6 +226,10 @@ if ($isServer) { } } +# --- Run legacy driver and GlobalFlag checks (server IPU blocker detection) --- +$legacyDriverBlockers = Get-LegacyDriverBlockers +$globalFlagEnabled = Get-GlobalFlagStatus + # --- Checklist Output --- $checklist = @() $checklist += "Windows Version: $windowsProductName" @@ -256,6 +284,30 @@ if ($isServer) { } } +# Legacy Driver / Ghost Hardware Check (IPU blocker) +if ($legacyDriverBlockers.Count -gt 0) { + $checklist += "[Failed] Legacy IPU-blocking drivers detected: $($legacyDriverBlockers -join ', ')" + $messages += "" + $messages += "FAILED: Legacy VMware/ghost hardware drivers are present and set to load. These will cause IPU to fail with error 0xC1900101-0x50016 (SafeOS boot crash)." + $messages += " Drivers found: $($legacyDriverBlockers -join ', ')" + $messages += " Action: In Device Manager, DISABLE (do not uninstall) each legacy device before retrying IPU." + $messages += " Note: Uninstalling may allow PnP to reinstall the driver on next reboot. Disable ensures it stays dormant during upgrade." +} else { + $checklist += "[Passed] No legacy IPU-blocking drivers detected" +} + +# GlobalFlag (PageHeap/debug mode) Check +if ($globalFlagEnabled) { + $checklist += "[Failed] GlobalFlag debug mode is enabled (causes throttled Setup and slow IPU rollback)" + $messages += "" + $messages += "FAILED: GlobalFlag is enabled in Session Manager. This forces Windows Setup to run in debug/PageHeap mode, causing severely throttled memory operations during IPU and a guaranteed rollback." + $messages += " Action: Disable GlobalFlag before retrying IPU:" + $messages += ' reg delete "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager" /v GlobalFlag /f' + $messages += " Then reboot the VM before attempting the in-place upgrade." +} else { + $checklist += "[Passed] GlobalFlag debug mode is not enabled" +} + # Output checklist first, then messages $checklist | ForEach-Object { Write-Output $_ } $messages | ForEach-Object { Write-Output $_ } From c7866284c8b9b46f0f46cc419149d331d0a20728 Mon Sep 17 00:00:00 2001 From: jdickson289 Date: Tue, 9 Jun 2026 14:23:29 -0500 Subject: [PATCH 02/17] Add simple offline TSS rescue wrapper and README --- .../Invoke-TSSOfflineRescueWrapper.ps1 | 209 ++++++++++++++++++ .../Windows/TSSOfflineRescueWrapper/readme.md | 43 ++++ 2 files changed, 252 insertions(+) create mode 100644 RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 create mode 100644 RunCommand/Windows/TSSOfflineRescueWrapper/readme.md diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 new file mode 100644 index 0000000..cbcbb5c --- /dev/null +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 @@ -0,0 +1,209 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$OfflineWindowsRoot, + + [Parameter(Mandatory = $false)] + [string]$OutputRoot = "C:\MS_DATA\OfflineTSSWrapper", + + [Parameter(Mandatory = $false)] + [string]$TssPath, + + [Parameter(Mandatory = $false)] + [string]$TssCollectLog = "DND_SetupReport", + + [Parameter(Mandatory = $false)] + [string[]]$TssArguments, + + [Parameter(Mandatory = $false)] + [switch]$RunTssOnRescueVm, + + [Parameter(Mandatory = $false)] + [switch]$IncludeRegistryHives, + + [Parameter(Mandatory = $false)] + [switch]$ZipOutput, + + [Parameter(Mandatory = $false)] + [switch]$NoAcceptEula, + + [Parameter(Mandatory = $false)] + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +function Resolve-OfflineWindowsRoot { + param( + [string]$RequestedPath + ) + + if (-not [string]::IsNullOrWhiteSpace($RequestedPath)) { + $normalized = $RequestedPath.TrimEnd("\\") + if (-not (Test-Path -LiteralPath $normalized)) { + throw "OfflineWindowsRoot path does not exist: $normalized" + } + + if (-not (Test-Path -LiteralPath (Join-Path $normalized "System32\config\SYSTEM"))) { + throw "Path does not look like a Windows directory (missing System32\\config\\SYSTEM): $normalized" + } + + return $normalized + } + + $drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -match "^[A-Z]:\\$" } + $candidates = New-Object System.Collections.Generic.List[string] + + foreach ($drive in $drives) { + $candidate = Join-Path $drive.Root "Windows" + $systemHive = Join-Path $candidate "System32\config\SYSTEM" + if (Test-Path -LiteralPath $systemHive) { + $candidates.Add($candidate) | Out-Null + } + } + + if ($candidates.Count -eq 0) { + throw "No offline Windows installation was auto-detected. Provide -OfflineWindowsRoot explicitly." + } + + if ($candidates.Count -gt 1) { + $first = $candidates[0] + Write-Warning "Multiple Windows roots detected: $($candidates -join ', '). Using: $first" + return $first + } + + return $candidates[0] +} + +function Copy-IfPresent { + param( + [Parameter(Mandatory = $true)] + [string]$Source, + + [Parameter(Mandatory = $true)] + [string]$Destination + ) + + if (-not (Test-Path -LiteralPath $Source)) { + Write-Host "[skip] Missing: $Source" -ForegroundColor DarkYellow + return + } + + $destParent = Split-Path -Parent $Destination + if (-not (Test-Path -LiteralPath $destParent)) { + New-Item -Path $destParent -ItemType Directory -Force | Out-Null + } + + Copy-Item -LiteralPath $Source -Destination $Destination -Recurse -Force + Write-Host "[copy] $Source -> $Destination" -ForegroundColor DarkCyan +} + +function Run-OptionalTss { + param( + [string]$Path, + [string[]]$Arguments, + [switch]$AutoAcceptEula + ) + + if (-not (Test-Path -LiteralPath $Path)) { + throw "TSS script not found: $Path" + } + + $invokeArgs = New-Object System.Collections.Generic.List[string] + if ($Arguments) { + foreach ($arg in $Arguments) { + if (-not [string]::IsNullOrWhiteSpace($arg)) { + $invokeArgs.Add($arg) | Out-Null + } + } + } + + if ($AutoAcceptEula -and -not ($invokeArgs -contains "-AcceptEula")) { + $invokeArgs.Add("-AcceptEula") | Out-Null + } + + Write-Host ("[tss] Starting TSS: {0} {1}" -f $Path, ($invokeArgs -join " ")) -ForegroundColor Yellow + & $Path @invokeArgs + Write-Host "[tss] Completed TSS collection." -ForegroundColor Green +} + +$resolvedWindowsRoot = Resolve-OfflineWindowsRoot -RequestedPath $OfflineWindowsRoot +$offlineRoot = Split-Path -Parent $resolvedWindowsRoot +$timeStamp = Get-Date -Format "yyyyMMdd-HHmmss" +$outputFolder = Join-Path $OutputRoot "offline-tss-wrapper-$timeStamp" + +if ((Test-Path -LiteralPath $outputFolder) -and -not $Force) { + throw "Output folder already exists: $outputFolder. Use -Force to overwrite." +} + +New-Item -Path $outputFolder -ItemType Directory -Force | Out-Null + +Write-Host "Offline Windows root : $resolvedWindowsRoot" -ForegroundColor Green +Write-Host "Offline disk root : $offlineRoot" -ForegroundColor Green +Write-Host "Output folder : $outputFolder" -ForegroundColor Green + +# Collect a practical offline bundle aligned with Windows Update / DnD troubleshooting. +$pathsToCollect = @( + @{ Rel = "Windows\System32\winevt\Logs"; Dest = "offline\winevt\Logs" }, + @{ Rel = "Windows\Logs\CBS"; Dest = "offline\Windows\Logs\CBS" }, + @{ Rel = "Windows\Logs\DISM"; Dest = "offline\Windows\Logs\DISM" }, + @{ Rel = "Windows\Panther"; Dest = "offline\Windows\Panther" }, + @{ Rel = "Windows\INF\setupapi.dev.log"; Dest = "offline\Windows\INF\setupapi.dev.log" }, + @{ Rel = "Windows\INF\setupapi.setup.log"; Dest = "offline\Windows\INF\setupapi.setup.log" }, + @{ Rel = "Windows\SoftwareDistribution\ReportingEvents.log"; Dest = "offline\Windows\SoftwareDistribution\ReportingEvents.log" }, + @{ Rel = "Windows\System32\catroot2"; Dest = "offline\Windows\System32\catroot2" }, + @{ Rel = "Windows\Minidump"; Dest = "offline\Windows\Minidump" }, + @{ Rel = "Windows\MEMORY.DMP"; Dest = "offline\Windows\MEMORY.DMP" }, + @{ Rel = "ProgramData\USOShared\Logs"; Dest = "offline\ProgramData\USOShared\Logs" } +) + +foreach ($item in $pathsToCollect) { + $sourcePath = Join-Path $offlineRoot $item.Rel + $destPath = Join-Path $outputFolder $item.Dest + Copy-IfPresent -Source $sourcePath -Destination $destPath +} + +if ($IncludeRegistryHives) { + $hiveFolder = Join-Path $resolvedWindowsRoot "System32\config" + $hives = @("SYSTEM", "SOFTWARE", "SAM", "SECURITY", "DEFAULT", "COMPONENTS") + foreach ($hive in $hives) { + $sourceHive = Join-Path $hiveFolder $hive + $destHive = Join-Path $outputFolder ("offline\registry\{0}" -f $hive) + Copy-IfPresent -Source $sourceHive -Destination $destHive + } +} + +if ($RunTssOnRescueVm) { + if ([string]::IsNullOrWhiteSpace($TssPath)) { + $candidate = Join-Path $PSScriptRoot "..\TSS\TSS.ps1" + if (Test-Path -LiteralPath $candidate) { + $TssPath = (Resolve-Path $candidate).Path + } + else { + throw "-RunTssOnRescueVm was set, but -TssPath was not provided and default candidate was not found: $candidate" + } + } + + $effectiveTssArgs = @() + if ($TssArguments -and $TssArguments.Count -gt 0) { + $effectiveTssArgs = $TssArguments + } + else { + $effectiveTssArgs = @("-CollectLog", $TssCollectLog) + } + + Run-OptionalTss -Path $TssPath -Arguments $effectiveTssArgs -AutoAcceptEula:(-not $NoAcceptEula) +} + +if ($ZipOutput) { + $zipPath = "$outputFolder.zip" + if (Test-Path -LiteralPath $zipPath) { + Remove-Item -LiteralPath $zipPath -Force + } + + Compress-Archive -Path (Join-Path $outputFolder "*") -DestinationPath $zipPath -CompressionLevel Optimal + Write-Host "Zip created: $zipPath" -ForegroundColor Green +} + +Write-Host "Offline rescue collection complete." -ForegroundColor Green +Write-Host "Bundle path: $outputFolder" -ForegroundColor Green diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md b/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md new file mode 100644 index 0000000..9cb58c0 --- /dev/null +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md @@ -0,0 +1,43 @@ +# TSS Offline Rescue Wrapper + +## Overview +This script helps when the target VM cannot boot and its OS disk is attached to a rescue VM: +- Copies offline Windows troubleshooting artifacts from the attached disk. +- Optionally runs TSS on the rescue VM. +- Accepts any TSS switch set using `-TssArguments`. +- No telemetry/metadata output files are generated by this wrapper. + +## Files +- `Invoke-TSSOfflineRescueWrapper.ps1` + +## Examples + +### Offline collection only +```powershell +.\Invoke-TSSOfflineRescueWrapper.ps1 -OfflineWindowsRoot F:\Windows -ZipOutput +``` + +### Offline collection + default TSS mode (`-CollectLog DND_SetupReport`) +```powershell +.\Invoke-TSSOfflineRescueWrapper.ps1 -OfflineWindowsRoot F:\Windows -RunTssOnRescueVm -TssPath C:\TSS\TSS.ps1 +``` + +### Offline collection + custom TSS switches +```powershell +.\Invoke-TSSOfflineRescueWrapper.ps1 -OfflineWindowsRoot F:\Windows -RunTssOnRescueVm -TssPath C:\TSS\TSS.ps1 -TssArguments @('-SDP','Setup','-SkipSDPList','skipBPA,skipTS') +``` + +## Key Parameters +- `-OfflineWindowsRoot `: Offline Windows directory (example `F:\Windows`). +- `-OutputRoot `: Root for output bundles. Default `C:\MS_DATA\OfflineTSSWrapper`. +- `-IncludeRegistryHives`: Include SYSTEM/SOFTWARE/SAM/SECURITY/DEFAULT/COMPONENTS hives. +- `-RunTssOnRescueVm`: Runs TSS in rescue VM context. +- `-TssPath `: Path to TSS script. +- `-TssArguments `: Any TSS args passed as-is. +- `-TssCollectLog `: Fallback used only if `-TssArguments` is omitted. +- `-NoAcceptEula`: Prevent automatic `-AcceptEula` append. +- `-ZipOutput`: Create zip after collection. + +## Notes +- This does not run TSS against an offline image directly; it collects offline artifacts and can run TSS on the rescue VM itself. +- Use elevated PowerShell on the rescue VM. From 0113999a43c9954e29a1f6586c56c7e5e65f61c2 Mon Sep 17 00:00:00 2001 From: John Dickson <28873200+jdickson289@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:40:27 -0500 Subject: [PATCH 03/17] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Invoke-TSSOfflineRescueWrapper.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 index cbcbb5c..a422a95 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 @@ -122,9 +122,10 @@ function Run-OptionalTss { $invokeArgs.Add("-AcceptEula") | Out-Null } - Write-Host ("[tss] Starting TSS: {0} {1}" -f $Path, ($invokeArgs -join " ")) -ForegroundColor Yellow - & $Path @invokeArgs - Write-Host "[tss] Completed TSS collection." -ForegroundColor Green +$invokeArgsArray = $invokeArgs.ToArray() +Write-Host ("[tss] Starting TSS: {0} {1}" -f $Path, ($invokeArgsArray -join " ")) -ForegroundColor Yellow +& $Path @invokeArgsArray +Write-Host "[tss] Completed TSS collection." -ForegroundColor Green } $resolvedWindowsRoot = Resolve-OfflineWindowsRoot -RequestedPath $OfflineWindowsRoot From da3cc809d13d6a0313767793b25643123c592108 Mon Sep 17 00:00:00 2001 From: John Dickson <28873200+jdickson289@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:40:37 -0500 Subject: [PATCH 04/17] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Invoke-TSSOfflineRescueWrapper.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 index a422a95..6c4b599 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 @@ -131,6 +131,11 @@ Write-Host "[tss] Completed TSS collection." -ForegroundColor Green $resolvedWindowsRoot = Resolve-OfflineWindowsRoot -RequestedPath $OfflineWindowsRoot $offlineRoot = Split-Path -Parent $resolvedWindowsRoot $timeStamp = Get-Date -Format "yyyyMMdd-HHmmss" + +if (-not (Test-Path -LiteralPath $OutputRoot)) { + New-Item -Path $OutputRoot -ItemType Directory -Force | Out-Null +} + $outputFolder = Join-Path $OutputRoot "offline-tss-wrapper-$timeStamp" if ((Test-Path -LiteralPath $outputFolder) -and -not $Force) { From e011d21d81e4e1cadcf4be54d711f591755c45b3 Mon Sep 17 00:00:00 2001 From: John Dickson <28873200+jdickson289@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:41:00 -0500 Subject: [PATCH 05/17] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Invoke-TSSOfflineRescueWrapper.ps1 | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 index 6c4b599..ed3c44d 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 @@ -51,28 +51,33 @@ function Resolve-OfflineWindowsRoot { return $normalized } - $drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -match "^[A-Z]:\\$" } - $candidates = New-Object System.Collections.Generic.List[string] - - foreach ($drive in $drives) { - $candidate = Join-Path $drive.Root "Windows" - $systemHive = Join-Path $candidate "System32\config\SYSTEM" - if (Test-Path -LiteralPath $systemHive) { - $candidates.Add($candidate) | Out-Null - } +$drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Root -match "^[A-Z]:\\$" } +$candidates = New-Object System.Collections.Generic.List[string] +$localWindows = (Resolve-Path -LiteralPath $env:windir).Path + +foreach ($drive in $drives) { + $candidate = Join-Path $drive.Root "Windows" + if ($candidate -ieq $localWindows) { + continue } - if ($candidates.Count -eq 0) { - throw "No offline Windows installation was auto-detected. Provide -OfflineWindowsRoot explicitly." + $systemHive = Join-Path $candidate "System32\config\SYSTEM" + if (Test-Path -LiteralPath $systemHive) { + $candidates.Add($candidate) | Out-Null } +} - if ($candidates.Count -gt 1) { - $first = $candidates[0] - Write-Warning "Multiple Windows roots detected: $($candidates -join ', '). Using: $first" - return $first - } +if ($candidates.Count -eq 0) { + throw "No offline Windows installation was auto-detected (local Windows is ignored). Provide -OfflineWindowsRoot explicitly." +} + +if ($candidates.Count -gt 1) { + $first = $candidates[0] + Write-Warning "Multiple offline Windows roots detected: $($candidates -join ', '). Using: $first" + return $first +} - return $candidates[0] +return $candidates[0] } function Copy-IfPresent { From 2a816f82d4319c543b7bee3de6ea5bddd6b24a73 Mon Sep 17 00:00:00 2001 From: jdickson289 Date: Fri, 12 Jun 2026 14:46:38 -0500 Subject: [PATCH 06/17] Handle root-level Windows path when deriving offline disk root --- .../TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 index ed3c44d..ff50520 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 @@ -135,6 +135,7 @@ Write-Host "[tss] Completed TSS collection." -ForegroundColor Green $resolvedWindowsRoot = Resolve-OfflineWindowsRoot -RequestedPath $OfflineWindowsRoot $offlineRoot = Split-Path -Parent $resolvedWindowsRoot +$offlineRoot = if ([string]::IsNullOrWhiteSpace($offlineRoot)) { Split-Path -Qualifier $resolvedWindowsRoot } else { $offlineRoot } $timeStamp = Get-Date -Format "yyyyMMdd-HHmmss" if (-not (Test-Path -LiteralPath $OutputRoot)) { From b91f3439395adbcb4ce5219b6ef09c25abe3cb03 Mon Sep 17 00:00:00 2001 From: jdickson289 Date: Fri, 12 Jun 2026 14:50:53 -0500 Subject: [PATCH 07/17] Continue collection when files are locked during copy --- .../Invoke-TSSOfflineRescueWrapper.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 index ff50520..96f7a68 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 @@ -99,8 +99,13 @@ function Copy-IfPresent { New-Item -Path $destParent -ItemType Directory -Force | Out-Null } - Copy-Item -LiteralPath $Source -Destination $Destination -Recurse -Force - Write-Host "[copy] $Source -> $Destination" -ForegroundColor DarkCyan + try { + Copy-Item -LiteralPath $Source -Destination $Destination -Recurse -Force -ErrorAction Stop + Write-Host "[copy] $Source -> $Destination" -ForegroundColor DarkCyan + } + catch { + Write-Warning "[skip] Failed to copy $Source. Reason: $($_.Exception.Message)" + } } function Run-OptionalTss { From daa06a62d16bfd71979fb2b48a6f3d84475e372e Mon Sep 17 00:00:00 2001 From: jdickson289 Date: Tue, 16 Jun 2026 14:01:31 -0500 Subject: [PATCH 08/17] Default TSS to -SDP Setup and fix AmbiguousParameterSet via hashtable splat --- .../Invoke-TSSOfflineRescueWrapper.ps1 | 223 +++++++++++++++--- .../Windows/TSSOfflineRescueWrapper/readme.md | 84 +++++-- 2 files changed, 255 insertions(+), 52 deletions(-) diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 index 96f7a68..32b68ad 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/Invoke-TSSOfflineRescueWrapper.ps1 @@ -4,20 +4,17 @@ param( [string]$OfflineWindowsRoot, [Parameter(Mandatory = $false)] - [string]$OutputRoot = "C:\MS_DATA\OfflineTSSWrapper", + [string]$Disk, [Parameter(Mandatory = $false)] [string]$TssPath, [Parameter(Mandatory = $false)] - [string]$TssCollectLog = "DND_SetupReport", + [string]$TssCollectLog, [Parameter(Mandatory = $false)] [string[]]$TssArguments, - [Parameter(Mandatory = $false)] - [switch]$RunTssOnRescueVm, - [Parameter(Mandatory = $false)] [switch]$IncludeRegistryHives, @@ -33,11 +30,129 @@ param( $ErrorActionPreference = "Stop" +$msDataRoot = "C:\MS_DATA" +$outputRoot = Join-Path $msDataRoot "TSS_PERF_OFFLINE" + +function Ensure-DiskReady { + param( + [int]$DiskNumber + ) + + $targetDisk = Get-Disk -Number $DiskNumber -ErrorAction SilentlyContinue + if (-not $targetDisk) { + throw "Disk number '$DiskNumber' was not found." + } + + if ($targetDisk.OperationalStatus -eq 'Offline' -or $targetDisk.IsOffline) { + Write-Host "[disk] Bringing disk $DiskNumber online" -ForegroundColor Yellow + try { + Set-Disk -Number $DiskNumber -IsOffline $false -ErrorAction Stop + Set-Disk -Number $DiskNumber -IsReadOnly $false -ErrorAction Stop + } + catch { + if ($_ -match 'Access Denied|40001') { + throw "Access denied bringing disk $DiskNumber online. The disk may be in use by another process or VM." + } + throw "Failed to bring disk $DiskNumber online: $($_.Exception.Message)" + } + Start-Sleep -Seconds 2 + $targetDisk = Get-Disk -Number $DiskNumber -ErrorAction SilentlyContinue + } + elseif ($targetDisk.IsReadOnly) { + Write-Host "[disk] Clearing read-only on disk $DiskNumber" -ForegroundColor Yellow + try { + Set-Disk -Number $DiskNumber -IsReadOnly $false -ErrorAction Stop + } + catch { + if ($_ -match 'Access Denied|40001') { + throw "Access denied clearing read-only on disk $DiskNumber. The disk may be in use by another process or VM." + } + throw "Failed to clear read-only flag on disk ${DiskNumber}: $($_.Exception.Message)" + } + $targetDisk = Get-Disk -Number $DiskNumber -ErrorAction SilentlyContinue + } + + if (-not $targetDisk -or $targetDisk.IsOffline -or $targetDisk.IsReadOnly) { + throw "Disk $DiskNumber is not ready (offline or read-only) after remediation attempt." + } +} + function Resolve-OfflineWindowsRoot { param( - [string]$RequestedPath + [string]$RequestedPath, + [string]$DiskSpecifier ) + if (-not [string]::IsNullOrWhiteSpace($RequestedPath) -and -not [string]::IsNullOrWhiteSpace($DiskSpecifier)) { + throw "Use either -OfflineWindowsRoot or -Disk, not both." + } + + if (-not [string]::IsNullOrWhiteSpace($DiskSpecifier)) { + $normalizedDisk = $DiskSpecifier.Trim().ToUpperInvariant() + + if ($normalizedDisk -match "^\d+$") { + $diskNumber = [int]$normalizedDisk + Ensure-DiskReady -DiskNumber $diskNumber + + $volumes = @(Get-Partition -DiskNumber $diskNumber -ErrorAction SilentlyContinue | + Get-Volume -ErrorAction SilentlyContinue | + Where-Object { -not [string]::IsNullOrWhiteSpace($_.DriveLetter) }) + + if ($volumes.Count -eq 0) { + throw "Disk number '$diskNumber' has no mounted volume with a drive letter." + } + + $candidates = @() + foreach ($volume in $volumes) { + $candidate = "$($volume.DriveLetter):\Windows" + if (Test-Path -LiteralPath (Join-Path $candidate "System32\config\SYSTEM")) { + $candidates += $candidate.TrimEnd("\\") + } + } + + if ($candidates.Count -eq 0) { + throw "Disk number '$diskNumber' does not contain a Windows OS volume (missing Windows\\System32\\config\\SYSTEM)." + } + + if ($candidates.Count -gt 1) { + Write-Warning "Multiple Windows roots found on disk ${diskNumber}: $($candidates -join ', '). Using: $($candidates[0])" + } + + return $candidates[0] + } + + if ($normalizedDisk -match "^[A-Z]$") { + $normalizedDisk = "${normalizedDisk}:" + } + elseif ($normalizedDisk -match "^[A-Z]:\\$") { + $normalizedDisk = $normalizedDisk.Substring(0, 2) + } + + if ($normalizedDisk -notmatch "^[A-Z]:$") { + throw "-Disk must be a disk number (for example '2') or drive letter like 'E', 'E:' or 'E:\\'. Provided: $DiskSpecifier" + } + + $driveLetter = $normalizedDisk.Substring(0, 1) + $partition = Get-Partition -DriveLetter $driveLetter -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $partition) { + throw "Drive '$normalizedDisk' was not found." + } + Ensure-DiskReady -DiskNumber $partition.DiskNumber + + $diskRoot = "$normalizedDisk\\" + $candidate = Join-Path $diskRoot "Windows" + + if (-not (Test-Path -LiteralPath $candidate)) { + throw "The specified disk does not contain a Windows folder: $candidate" + } + + if (-not (Test-Path -LiteralPath (Join-Path $candidate "System32\config\SYSTEM"))) { + throw "Disk '$normalizedDisk' does not appear to be a Windows OS disk (missing System32\\config\\SYSTEM under $candidate)." + } + + return $candidate.TrimEnd("\\") + } + if (-not [string]::IsNullOrWhiteSpace($RequestedPath)) { $normalized = $RequestedPath.TrimEnd("\\") if (-not (Test-Path -LiteralPath $normalized)) { @@ -119,35 +234,63 @@ function Run-OptionalTss { throw "TSS script not found: $Path" } - $invokeArgs = New-Object System.Collections.Generic.List[string] + $tokens = @() if ($Arguments) { foreach ($arg in $Arguments) { if (-not [string]::IsNullOrWhiteSpace($arg)) { - $invokeArgs.Add($arg) | Out-Null + $tokens += $arg.Trim() } } } - if ($AutoAcceptEula -and -not ($invokeArgs -contains "-AcceptEula")) { - $invokeArgs.Add("-AcceptEula") | Out-Null + if ($AutoAcceptEula -and -not ($tokens -contains "-AcceptEula")) { + $tokens += "-AcceptEula" } -$invokeArgsArray = $invokeArgs.ToArray() -Write-Host ("[tss] Starting TSS: {0} {1}" -f $Path, ($invokeArgsArray -join " ")) -ForegroundColor Yellow -& $Path @invokeArgsArray -Write-Host "[tss] Completed TSS collection." -ForegroundColor Green + # Parse the flat token list into a parameter hashtable so TSS is invoked by + # named parameters. Array splatting (& $Path @array) does NOT reliably + # resolve TSS's parameter sets and raises AmbiguousParameterSet; hashtable + # splatting binds parameters by name exactly like an interactive command + # line and resolves the parameter set correctly. + $tssParams = [ordered]@{} + $i = 0 + while ($i -lt $tokens.Count) { + $tok = $tokens[$i] + if ($tok -like "-*") { + $name = $tok.TrimStart("-") + if (($i + 1) -lt $tokens.Count -and ($tokens[$i + 1] -notlike "-*")) { + $tssParams[$name] = $tokens[$i + 1] + $i += 2 + } + else { + $tssParams[$name] = $true + $i += 1 + } + } + else { + $i += 1 + } + } + + Write-Host ("[tss] Starting TSS: {0} {1}" -f $Path, ($tokens -join " ")) -ForegroundColor Yellow + & $Path @tssParams + Write-Host "[tss] Completed TSS collection." -ForegroundColor Green } -$resolvedWindowsRoot = Resolve-OfflineWindowsRoot -RequestedPath $OfflineWindowsRoot +$resolvedWindowsRoot = Resolve-OfflineWindowsRoot -RequestedPath $OfflineWindowsRoot -DiskSpecifier $Disk $offlineRoot = Split-Path -Parent $resolvedWindowsRoot $offlineRoot = if ([string]::IsNullOrWhiteSpace($offlineRoot)) { Split-Path -Qualifier $resolvedWindowsRoot } else { $offlineRoot } -$timeStamp = Get-Date -Format "yyyyMMdd-HHmmss" +$timeStamp = Get-Date -Format "yyyyMMdd-HHmmss-fff" + +if (-not (Test-Path -LiteralPath $msDataRoot)) { + New-Item -Path $msDataRoot -ItemType Directory -Force | Out-Null +} -if (-not (Test-Path -LiteralPath $OutputRoot)) { - New-Item -Path $OutputRoot -ItemType Directory -Force | Out-Null +if (-not (Test-Path -LiteralPath $outputRoot)) { + New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null } -$outputFolder = Join-Path $OutputRoot "offline-tss-wrapper-$timeStamp" +$outputFolder = Join-Path $outputRoot "offline-tss-wrapper-$timeStamp" if ((Test-Path -LiteralPath $outputFolder) -and -not $Force) { throw "Output folder already exists: $outputFolder. Use -Force to overwrite." @@ -157,6 +300,8 @@ New-Item -Path $outputFolder -ItemType Directory -Force | Out-Null Write-Host "Offline Windows root : $resolvedWindowsRoot" -ForegroundColor Green Write-Host "Offline disk root : $offlineRoot" -ForegroundColor Green +Write-Host "MS_DATA root : $msDataRoot" -ForegroundColor Green +Write-Host "Output root : $outputRoot" -ForegroundColor Green Write-Host "Output folder : $outputFolder" -ForegroundColor Green # Collect a practical offline bundle aligned with Windows Update / DnD troubleshooting. @@ -190,27 +335,36 @@ if ($IncludeRegistryHives) { } } -if ($RunTssOnRescueVm) { - if ([string]::IsNullOrWhiteSpace($TssPath)) { - $candidate = Join-Path $PSScriptRoot "..\TSS\TSS.ps1" - if (Test-Path -LiteralPath $candidate) { - $TssPath = (Resolve-Path $candidate).Path - } - else { - throw "-RunTssOnRescueVm was set, but -TssPath was not provided and default candidate was not found: $candidate" - } - } +if ([string]::IsNullOrWhiteSpace($TssPath)) { + $candidate = Join-Path $PSScriptRoot "TSS\TSS.ps1" + Write-Host "[tss] -TssPath not provided. Checking default path: $candidate" -ForegroundColor Yellow - $effectiveTssArgs = @() - if ($TssArguments -and $TssArguments.Count -gt 0) { - $effectiveTssArgs = $TssArguments + if (Test-Path -LiteralPath $candidate) { + $TssPath = (Resolve-Path $candidate).Path + Write-Host "[tss] Found default TSS script: $TssPath" -ForegroundColor Green } else { - $effectiveTssArgs = @("-CollectLog", $TssCollectLog) + Write-Host "[tss] Default TSS script was not found: $candidate" -ForegroundColor Red + Write-Host "[tss] Expected layout:" -ForegroundColor Yellow + Write-Host " $PSScriptRoot" -ForegroundColor Yellow + Write-Host " $PSScriptRoot\TSS\TSS.ps1" -ForegroundColor Yellow + Write-Host "[tss] Action: unzip the TSS folder in the same directory as this wrapper, or provide -TssPath explicitly." -ForegroundColor Yellow + throw "-TssPath was not provided and wrapper-local default was not found: $candidate" } +} - Run-OptionalTss -Path $TssPath -Arguments $effectiveTssArgs -AutoAcceptEula:(-not $NoAcceptEula) +$effectiveTssArgs = @() +if ($TssArguments -and $TssArguments.Count -gt 0) { + $effectiveTssArgs = $TssArguments +} +elseif (-not [string]::IsNullOrWhiteSpace($TssCollectLog)) { + $effectiveTssArgs = @("-CollectLog", $TssCollectLog) } +else { + $effectiveTssArgs = @("-SDP", "Setup") +} + +Run-OptionalTss -Path $TssPath -Arguments $effectiveTssArgs -AutoAcceptEula:(-not $NoAcceptEula) if ($ZipOutput) { $zipPath = "$outputFolder.zip" @@ -223,4 +377,5 @@ if ($ZipOutput) { } Write-Host "Offline rescue collection complete." -ForegroundColor Green +Write-Host "Data location: $outputRoot" -ForegroundColor Green Write-Host "Bundle path: $outputFolder" -ForegroundColor Green diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md b/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md index 9cb58c0..4041dc1 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md @@ -1,43 +1,91 @@ # TSS Offline Rescue Wrapper ## Overview -This script helps when the target VM cannot boot and its OS disk is attached to a rescue VM: +This script is for rescue-VM scenarios where the broken VM OS disk is attached offline: - Copies offline Windows troubleshooting artifacts from the attached disk. -- Optionally runs TSS on the rescue VM. -- Accepts any TSS switch set using `-TssArguments`. -- No telemetry/metadata output files are generated by this wrapper. +- Always runs TSS on the rescue VM context. +- Uses default TSS mode `-SDP Setup` when no explicit TSS arguments are provided. +- Supports explicit `-TssCollectLog` or full pass-through `-TssArguments`. +- Uses fixed output root `C:\MS_DATA\TSS_PERF_OFFLINE`. -## Files +## File - `Invoke-TSSOfflineRescueWrapper.ps1` +## TSS Folder Expectation (Default) +If `-TssPath` is not provided, the script expects: + +```text +\TSS\TSS.ps1 +``` + +Example: + +```text +RunCommand\Windows\TSSOfflineRescueWrapper\Invoke-TSSOfflineRescueWrapper.ps1 +RunCommand\Windows\TSSOfflineRescueWrapper\TSS\TSS.ps1 +``` + +If not present, the script prints screen guidance and stops. + +Disk selection note: `-Disk 2` in examples is sample syntax. Replace `2` with the disk number that contains the offline OS. + ## Examples -### Offline collection only +### Default run (uses `-SDP Setup`) +```powershell +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -ZipOutput +``` + +### DND setup report ```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -OfflineWindowsRoot F:\Windows -ZipOutput +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssCollectLog DND_SetupReport -ZipOutput ``` -### Offline collection + default TSS mode (`-CollectLog DND_SetupReport`) +### TSS UEX example ```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -OfflineWindowsRoot F:\Windows -RunTssOnRescueVm -TssPath C:\TSS\TSS.ps1 +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-UEX_RDSsrv') -ZipOutput ``` -### Offline collection + custom TSS switches +### Directory Services (DS) example ```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -OfflineWindowsRoot F:\Windows -RunTssOnRescueVm -TssPath C:\TSS\TSS.ps1 -TssArguments @('-SDP','Setup','-SkipSDPList','skipBPA,skipTS') +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-SDP','Dom') -ZipOutput ``` -## Key Parameters +### Use disk number auto-detection +```powershell +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssCollectLog DND_SetupReport -ZipOutput +``` + +### Use wrapper-local default TSS path +```powershell +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -ZipOutput +``` + +### Custom TSS switches +```powershell +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-SDP','Setup') -ZipOutput +``` + +## Parameters - `-OfflineWindowsRoot `: Offline Windows directory (example `F:\Windows`). -- `-OutputRoot `: Root for output bundles. Default `C:\MS_DATA\OfflineTSSWrapper`. -- `-IncludeRegistryHives`: Include SYSTEM/SOFTWARE/SAM/SECURITY/DEFAULT/COMPONENTS hives. -- `-RunTssOnRescueVm`: Runs TSS in rescue VM context. -- `-TssPath `: Path to TSS script. +- `-Disk `: Disk selector, supports disk number (`2`) or drive (`E`, `E:`, `E:\`). +- `-TssPath `: Optional explicit path to `TSS.ps1`. If omitted, wrapper-local default is used. +- `-TssCollectLog `: Optional override to run `-CollectLog `. - `-TssArguments `: Any TSS args passed as-is. -- `-TssCollectLog `: Fallback used only if `-TssArguments` is omitted. +- `-IncludeRegistryHives`: Include SYSTEM/SOFTWARE/SAM/SECURITY/DEFAULT/COMPONENTS hives. - `-NoAcceptEula`: Prevent automatic `-AcceptEula` append. - `-ZipOutput`: Create zip after collection. +- `-Force`: Allow overwrite when output folder already exists. + +## Output +- Root: `C:\MS_DATA\TSS_PERF_OFFLINE` +- Run folder: `offline-tss-wrapper-` +- Optional zip: `offline-tss-wrapper-.zip` ## Notes -- This does not run TSS against an offline image directly; it collects offline artifacts and can run TSS on the rescue VM itself. +- The script no longer uses `-RunTssOnRescueVm`. +- The script no longer uses `-OutputRoot`. +- If neither `-TssArguments` nor `-TssCollectLog` is provided, wrapper defaults to `-SDP Setup`. +- Note: `-SDP Perf` is deprecated in current TSS; use `-SDP Setup`. +- DS (Directory Services) SDP key is `Dom` in TSS (`-SDP Dom`). - Use elevated PowerShell on the rescue VM. From 315106c734748d980ce8c397ece64d425795172d Mon Sep 17 00:00:00 2001 From: jdickson289 Date: Wed, 17 Jun 2026 11:34:25 -0500 Subject: [PATCH 09/17] Reformat README to repo house style; rename to TSS Offline Log Collector --- .../Windows/TSSOfflineRescueWrapper/readme.md | 82 +++++++++++-------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md b/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md index 4041dc1..cb5f28e 100644 --- a/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md +++ b/RunCommand/Windows/TSSOfflineRescueWrapper/readme.md @@ -1,17 +1,30 @@ -# TSS Offline Rescue Wrapper +# TSS Offline Log Collector -## Overview -This script is for rescue-VM scenarios where the broken VM OS disk is attached offline: -- Copies offline Windows troubleshooting artifacts from the attached disk. -- Always runs TSS on the rescue VM context. -- Uses default TSS mode `-SDP Setup` when no explicit TSS arguments are provided. -- Supports explicit `-TssCollectLog` or full pass-through `-TssArguments`. -- Uses fixed output root `C:\MS_DATA\TSS_PERF_OFFLINE`. +This PowerShell script is for rescue-VM scenarios where the broken VM OS disk is attached offline to a working rescue VM. It collects offline Windows troubleshooting logs from the attached disk and then runs TSS in the rescue VM context. -## File -- `Invoke-TSSOfflineRescueWrapper.ps1` +## What It Does + +1. **Offline Log Collection** + - Copies offline Windows troubleshooting logs from the attached disk (event logs, CBS, DISM, Panther, setupapi, Software Distribution, catroot2, USO logs, and optional registry hives). + +2. **TSS Execution** + - Always runs TSS on the rescue VM context. + - Uses default TSS mode `-SDP Setup` when no explicit TSS arguments are provided. + - Supports explicit `-TssCollectLog` or full pass-through `-TssArguments`. + +3. **Output Packaging** + - Writes to fixed output root `C:\MS_DATA\TSS_PERF_OFFLINE`. + - Optionally creates a zip of the run folder. + +## Prerequisites + +- PowerShell 5.1 or higher. +- **Run from an elevated (Run as administrator) PowerShell console.** +- **Must run in the standard PowerShell console host (`ConsoleHost`), not PowerShell ISE.** +- The broken VM OS disk must already be attached to the rescue VM. ## TSS Folder Expectation (Default) + If `-TssPath` is not provided, the script expects: ```text @@ -29,44 +42,38 @@ If not present, the script prints screen guidance and stops. Disk selection note: `-Disk 2` in examples is sample syntax. Replace `2` with the disk number that contains the offline OS. -## Examples +## Usage -### Default run (uses `-SDP Setup`) -```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -ZipOutput -``` +From an elevated PowerShell console, in the directory that contains the script: -### DND setup report ```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssCollectLog DND_SetupReport -ZipOutput +Set-ExecutionPolicy Bypass -Force ``` -### TSS UEX example -```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-UEX_RDSsrv') -ZipOutput -``` +## Quick Start -### Directory Services (DS) example +### Default is Setup/Perf Report ```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-SDP','Dom') -ZipOutput +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -ZipOutput ``` -### Use disk number auto-detection +### DND setup report ```powershell .\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssCollectLog DND_SetupReport -ZipOutput ``` -### Use wrapper-local default TSS path +### UEX Report ```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -ZipOutput +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-UEX_RDSsrv') -ZipOutput ``` -### Custom TSS switches +### Directory Services (DS) report ```powershell -.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-SDP','Setup') -ZipOutput +.\Invoke-TSSOfflineRescueWrapper.ps1 -Disk 2 -TssPath C:\Tools\TSS\TSS.ps1 -TssArguments @('-SDP','Dom') -ZipOutput ``` ## Parameters + - `-OfflineWindowsRoot `: Offline Windows directory (example `F:\Windows`). - `-Disk `: Disk selector, supports disk number (`2`) or drive (`E`, `E:`, `E:\`). - `-TssPath `: Optional explicit path to `TSS.ps1`. If omitted, wrapper-local default is used. @@ -78,14 +85,25 @@ Disk selection note: `-Disk 2` in examples is sample syntax. Replace `2` with th - `-Force`: Allow overwrite when output folder already exists. ## Output + - Root: `C:\MS_DATA\TSS_PERF_OFFLINE` - Run folder: `offline-tss-wrapper-` - Optional zip: `offline-tss-wrapper-.zip` ## Notes -- The script no longer uses `-RunTssOnRescueVm`. -- The script no longer uses `-OutputRoot`. + - If neither `-TssArguments` nor `-TssCollectLog` is provided, wrapper defaults to `-SDP Setup`. -- Note: `-SDP Perf` is deprecated in current TSS; use `-SDP Setup`. +- `-SDP Perf` is deprecated in current TSS; use `-SDP Setup`. - DS (Directory Services) SDP key is `Dom` in TSS (`-SDP Dom`). -- Use elevated PowerShell on the rescue VM. + +## Known Issues + +- TSS SDP collection must run in the standard PowerShell console host. Running directly inside PowerShell ISE fails because the SDP module depends on console-host-only properties. Use a standard elevated PowerShell console, or invoke with `powershell -ExecutionPolicy Bypass -File