VM-Pilot.psm1
|
# VM-Pilot.psm1 -- module entry. For now this just exposes Start-VMPilot, which # spawns the existing VMPilot.GUI.ps1 in a new (hidden) PowerShell process so # the GUI's auto-elevation and hidden-console behavior continue to work as # they do when the user double-clicks VMPilot.bat. # # Future work will refactor VMPilot.GUI.ps1 into proper exported functions # (Get-VMPilotHWID, Remove-VMPilotVM, etc.) for scriptable use without the GUI. $script:ModuleRoot = $PSScriptRoot function Get-VMPilotLatestVersion { <# .SYNOPSIS Returns the newest VM-Pilot version published to PowerShell Gallery. .DESCRIPTION The Gallery's package page 302-redirects to the versioned URL, so the Location header carries the latest version -- much faster than Find-Module, which pulls a full package record. Returns $null on any failure; callers treat that as "don't know, carry on". #> [CmdletBinding()] param([int]$TimeoutSec = 5) try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { } $url = 'https://www.powershellgallery.com/packages/VM-Pilot' $location = $null try { # With redirects capped at 0, PowerShell 7 treats the 302 as an error and # throws HttpResponseException -- so the Location header has to come off # the exception's response, and the normal-return read below is only a # guard in case a future build stops throwing. (Windows PowerShell 5.1 # throws with no .Response at all; this module is PS7-only by manifest.) $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -MaximumRedirection 0 ` -TimeoutSec $TimeoutSec -ErrorAction Stop $location = $resp.Headers['Location'] } catch { $r = $_.Exception.Response if ($r -and $r.Headers) { try { $location = $r.Headers.GetValues('Location') } catch { } } } if ($location -is [array]) { $location = $location | Select-Object -First 1 } if ([string]::IsNullOrWhiteSpace($location)) { return $null } try { return [version](($location -split '/')[-1]) } catch { return $null } } function Show-VMPilotUpdateNotification { <# .SYNOPSIS Prompts to update VM-Pilot, and performs the update when accepted. .OUTPUTS [bool] $true when the module was updated and the caller should stop (the copy on disk is now newer than the one loaded in this session). #> [CmdletBinding()] param( [Parameter(Mandatory)][version]$CurrentVersion, [Parameter(Mandatory)][version]$LatestVersion ) Write-Host '' Write-Host "[!] VM-Pilot update available: $CurrentVersion -> $LatestVersion" -ForegroundColor Red Write-Host '' $response = Read-Host 'Update now? (Y/N) [Press Enter to skip]' if ($response -notmatch '^(y|Y)$') { if ($response -match '^(n|N)$') { Write-Host 'Skipping update.' -ForegroundColor Yellow } Write-Host '' return $false } Write-Host '' Write-Host 'Updating VM-Pilot...' -ForegroundColor Cyan try { # Update with whichever package manager actually installed the module -- # mixing them leaves duplicate side-by-side copies. $viaPSResource = $null if (Get-Command Get-InstalledPSResource -ErrorAction SilentlyContinue) { $viaPSResource = Get-InstalledPSResource -Name VM-Pilot -ErrorAction SilentlyContinue } $viaPowerShellGet = $null if (Get-Command Get-InstalledModule -ErrorAction SilentlyContinue) { $viaPowerShellGet = Get-InstalledModule -Name VM-Pilot -ErrorAction SilentlyContinue } if ($viaPSResource) { $installPath = ($viaPSResource | Select-Object -First 1).InstalledLocation $scope = if ($installPath -match 'Program Files') { 'AllUsers' } else { 'CurrentUser' } Update-PSResource -Name VM-Pilot -Scope $scope -Confirm:$false } elseif ($viaPowerShellGet -or (Get-Command Update-Module -ErrorAction SilentlyContinue)) { Update-Module -Name VM-Pilot -Force } else { Write-Host 'No update command available. Install manually with:' -ForegroundColor Yellow Write-Host ' Install-Module -Name VM-Pilot -Force' -ForegroundColor Yellow Write-Host '' return $false } } catch { Write-Host '' Write-Host "Update failed: $($_.Exception.Message)" -ForegroundColor Red Write-Host 'Update manually with:' -ForegroundColor Yellow Write-Host ' Update-Module -Name VM-Pilot -Force (installed via Install-Module)' -ForegroundColor Yellow Write-Host ' Update-PSResource -Name VM-Pilot (installed via Install-PSResource)' -ForegroundColor Yellow Write-Host '' return $false } Write-Host '' Write-Host 'Update complete!' -ForegroundColor Green Write-Host 'Restart PowerShell, then run Start-VMPilot again.' -ForegroundColor Green Write-Host '' return $true } function Test-VMPilotUpdate { <# .SYNOPSIS Checks PowerShell Gallery for a newer VM-Pilot and offers to install it. .DESCRIPTION Runs on each Start-VMPilot. Every failure path is silent -- a Gallery outage or offline host must never block launching the GUI. Set $env:VMPILOT_DISABLE_UPDATE_CHECK = 'true' to skip the check entirely. .OUTPUTS [bool] $true when an update was installed and the caller should stop. #> [CmdletBinding()] param() try { if ($env:VMPILOT_DISABLE_UPDATE_CHECK -eq 'true') { return $false } $current = Get-Module -Name VM-Pilot | Sort-Object Version -Descending | Select-Object -First 1 if (-not $current) { $current = Get-Module -Name VM-Pilot -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 } # Running straight from a clone rather than an installed module: nothing # to compare against, and Update-Module couldn't service it anyway. if (-not $current) { return $false } $latest = Get-VMPilotLatestVersion if (-not $latest) { return $false } if ($current.Version -lt $latest) { return (Show-VMPilotUpdateNotification -CurrentVersion $current.Version -LatestVersion $latest) } } catch { } return $false } function Start-VMPilot { <# .SYNOPSIS Launch the VM-Pilot host GUI. .DESCRIPTION Opens the dark-themed WPF window for creating a fresh Hyper-V VM and collecting its AutoPilot hardware hash. Choose Offline (CSV on disk) or Online (Intune AutoPilot import via the community script) mode in the GUI. The GUI auto-elevates to Administrator via UAC if needed. The PowerShell session that invoked Start-VMPilot is NOT affected -- the GUI runs in a separate hidden PowerShell process. This cmdlet returns immediately once the GUI has been launched. .EXAMPLE PS> Start-VMPilot Opens the host GUI. From there pick a mode + VM name and click the button. .LINK https://github.com/markorr321/VM-Pilot #> [CmdletBinding()] param() # ===== Update check ===== # Must run before the GUI spawns: WScript.Shell.Run detaches the GUI and # this function returns immediately, so the console prompt has to happen # while this session still owns the terminal. Returns $true only when an # update was installed -- the module on disk is then newer than the one # loaded here, so launching the stale in-memory GUI would be wrong. if (Test-VMPilotUpdate) { return } # ===== Pre-req check #1: Hyper-V ===== # Fires before the GUI process spawns so the user sees console feedback # immediately on `Start-VMPilot` (before any UAC popup). The GUI itself # repeats this check with a full graphical enable + reboot flow once # elevated, but this preview lets the user know what to expect. if (-not (Get-Command Get-VM -ErrorAction SilentlyContinue)) { Write-Host '' Write-Host '[VM-Pilot] Pre-req check: Hyper-V is not enabled on this machine.' -ForegroundColor Yellow Write-Host '[VM-Pilot] Launching the GUI — it will request elevation and offer to' -ForegroundColor Yellow Write-Host '[VM-Pilot] enable Hyper-V (reboot required) before opening the main window.' -ForegroundColor Yellow Write-Host '' } $guiPath = Join-Path $script:ModuleRoot 'VMPilot.GUI.ps1' if (-not (Test-Path $guiPath -PathType Leaf)) { throw "VMPilot.GUI.ps1 not found at $guiPath. Reinstall the VM-Pilot module." } # PS7-only module (enforced by the manifest), so the GUI always gets pwsh. $psExe = 'pwsh.exe' # Use WScript.Shell.Run rather than Start-Process so the console host # never paints. Start-Process -WindowStyle Hidden hides the window AFTER # it appears, producing a brief flash; .Run(..., 0, ...) tells the shell # to create the window in SW_HIDE from the start, so nothing flashes. $argLine = '-NoProfile -ExecutionPolicy Bypass -File "{0}"' -f $guiPath $wshell = New-Object -ComObject WScript.Shell try { [void]$wshell.Run(('"{0}" {1}' -f $psExe, $argLine), 0, $false) } finally { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($wshell) } } Export-ModuleMember -Function Start-VMPilot |