Private/Mount-Win32ToolkitIso.ps1
|
function Mount-Win32ToolkitIso { <# .SYNOPSIS Mounts a Windows ISO robustly: hydrates/stages a local copy when the source is a OneDrive placeholder (or the in-place mount fails), and retries transient mount failures. Returns the drive letter plus the path actually mounted and any staged temp copy to clean up. .DESCRIPTION Mount-DiskImage reads the ISO through the filesystem, so a OneDrive Files-On-Demand *placeholder* (or slow/redirected storage — common in a Downloads folder on a Windows 365 Cloud PC) makes it stall and fail with "The semaphore timeout period has expired." This helper: 1. If the file is marked Offline (a cloud placeholder), stages a hydrated local copy up front. 2. Mounts with a small retry (dismounting any half-attached image between attempts). 3. If an in-place mount still fails and we haven't staged yet, stages a local copy and retries. The caller dismounts ImagePath and deletes StagedPath (when set) in its finally. HOST-ONLY. .PARAMETER IsoPath Path to the Windows ISO. .PARAMETER MountRetries Attempts per mount target before giving up. Default 3. .PARAMETER StageDirectory Where a staged copy is written. Default: the system TEMP folder (local fixed disk). .PARAMETER NoStage Never stage a local copy (mount in place only, still with retries). .OUTPUTS PSCustomObject: DriveLetter (char), ImagePath (string, the path that is mounted), StagedPath (string or $null — a temp copy the caller must delete). #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$IsoPath, [ValidateRange(1, 10)] [int]$MountRetries = 3, [string]$StageDirectory = ([System.IO.Path]::GetTempPath()), [switch]$NoStage ) if (-not (Test-Path -LiteralPath $IsoPath)) { throw "ISO not found: $IsoPath" } $item = Get-Item -LiteralPath $IsoPath # Copy the ISO to a local temp file, guarding on free space. Returns the staged path. $stage = { param([string]$src) $dst = Join-Path $StageDirectory ('w32iso_' + [guid]::NewGuid().ToString('N').Substring(0, 8) + '.iso') try { $need = [int64]((Get-Item -LiteralPath $src).Length) $free = (Get-PSDrive -Name ($dst.Substring(0, 1)) -ErrorAction SilentlyContinue).Free # -ne $null (not truthiness): a genuinely full drive reports Free = 0, which must still trip the guard. if ($null -ne $free -and $free -lt ($need * 1.1)) { throw ("Not enough free space at '$StageDirectory' to stage the ISO (need ~{0:N1} GB, free ~{1:N1} GB)." -f ($need / 1GB), ($free / 1GB)) } } catch { if ($_.Exception.Message -like 'Not enough free space*') { throw } } # space error is fatal; probing errors are not Write-Verbose "Staging ISO to local disk ($dst)..." try { Copy-Item -LiteralPath $src -Destination $dst -Force -ErrorAction Stop } catch { # Copy-Item does NOT roll back a partial write. On the OneDrive/slow storage this targets, a # recall failure can leave a partial multi-GB temp file whose path we're about to lose (the # block throws before $dst is returned, so the outer catch's $staged is still $null). Delete it here. Remove-Item -LiteralPath $dst -Force -ErrorAction SilentlyContinue throw } $dst } # Mount one path with retries; dismount a half-attached image between tries. Throws if all fail. $mount = { param([string]$path) for ($i = 1; $i -le $MountRetries; $i++) { try { $img = Mount-DiskImage -ImagePath $path -StorageType ISO -PassThru -ErrorAction Stop $dl = ($img | Get-Volume).DriveLetter if ($dl) { return $dl } throw 'ISO mounted but no drive letter was assigned.' } catch { Dismount-DiskImage -ImagePath $path -ErrorAction SilentlyContinue | Out-Null if ($i -ge $MountRetries) { throw } Write-Warning "ISO mount attempt $i/$MountRetries failed ($($_.Exception.Message)); retrying..." Start-Sleep -Seconds ([Math]::Min(10, 2 * $i)) } } } $staged = $null $source = $IsoPath try { # A cloud placeholder will reliably time out — stage before the first attempt. if (-not $NoStage -and ($item.Attributes -band [System.IO.FileAttributes]::Offline)) { Write-Warning "ISO '$IsoPath' is a OneDrive/offline placeholder — staging a local copy first." $staged = & $stage $IsoPath $source = $staged } try { $drive = & $mount $source } catch { if (-not $staged -and -not $NoStage) { Write-Warning "Mounting '$IsoPath' in place failed ($($_.Exception.Message)). Staging a local copy and retrying..." $staged = & $stage $IsoPath $source = $staged $drive = & $mount $source # if this throws too, let it propagate } else { throw } } } catch { # We created a staged copy but never return the object, so the caller's finally can't clean it up. # Delete the (potentially multi-GB) temp copy here before resurfacing the error. if ($staged) { Remove-Item -LiteralPath $staged -Force -ErrorAction SilentlyContinue } throw } [pscustomobject]@{ DriveLetter = $drive ImagePath = $source StagedPath = $staged } } |