Private/Logic/Eigenverft.Manifested.Sandbox.Runtime.VCRuntime.ps1
|
<#
Eigenverft.Manifested.Sandbox.Cmd.VCRuntimeAndCache #> function ConvertTo-VCRuntimeVersion { <# .SYNOPSIS Normalizes VC runtime version text into a comparable version object. .DESCRIPTION Extracts a semantic VC runtime version from installer metadata or registry text and returns it as a System.Version when the input can be parsed. .PARAMETER VersionText Raw version text reported by the VC runtime installer or registry. .EXAMPLE ConvertTo-VCRuntimeVersion -VersionText '14.40.33810.0' #> [CmdletBinding()] param( [string]$VersionText ) if ([string]::IsNullOrWhiteSpace($VersionText)) { return $null } $match = [regex]::Match($VersionText, '\d+(?:\.\d+){1,3}') if (-not $match.Success) { return $null } return [version]$match.Value } function Format-VCRuntimeProcessArgument { <# .SYNOPSIS Formats a VC runtime installer argument for Start-Process. .DESCRIPTION Quotes installer argument values when they contain whitespace or quotes so the silent installer receives the expected log-path argument. .PARAMETER Value Argument value that may need quoting before process invocation. .EXAMPLE Format-VCRuntimeProcessArgument -Value 'C:\temp\vc runtime.log' #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Value ) if ($Value.IndexOfAny([char[]]@(' ', "`t", '"')) -ge 0) { return '"' + ($Value -replace '"', '\"') + '"' } return $Value } function Get-VCRuntimeInstallerInfo { <# .SYNOPSIS Builds the managed VC runtime installer metadata. .DESCRIPTION Returns the static download URL, cache path, and architecture metadata used by the sandbox when acquiring the VC++ redistributable bootstrapper. .PARAMETER LocalRoot Sandbox local root used to resolve the managed cache layout. .EXAMPLE Get-VCRuntimeInstallerInfo #> [CmdletBinding()] param( [string]$LocalRoot = (Get-ManifestedLocalRoot) ) $layout = Get-ManifestedLayout -LocalRoot $LocalRoot [pscustomobject]@{ Architecture = 'x64' FileName = 'vc_redist.x64.exe' DownloadUrl = 'https://aka.ms/vc14/vc_redist.x64.exe' CachePath = (Join-Path $layout.VCRuntimeCacheRoot 'vc_redist.x64.exe') } } function Get-CachedVCRuntimeInstaller { <# .SYNOPSIS Returns the currently cached VC runtime installer, if present. .DESCRIPTION Inspects the managed VC runtime cache location, reads file-version metadata from the cached bootstrapper, and returns normalized cache details. .PARAMETER LocalRoot Sandbox local root used to resolve the managed installer cache. .EXAMPLE Get-CachedVCRuntimeInstaller #> [CmdletBinding()] param( [string]$LocalRoot = (Get-ManifestedLocalRoot) ) $info = Get-VCRuntimeInstallerInfo -LocalRoot $LocalRoot if (-not (Test-Path -LiteralPath $info.CachePath)) { return $null } $item = Get-Item -LiteralPath $info.CachePath $versionObject = ConvertTo-VCRuntimeVersion -VersionText $item.VersionInfo.FileVersion [pscustomobject]@{ Architecture = $info.Architecture FileName = $info.FileName Path = $info.CachePath Version = if ($versionObject) { $versionObject.ToString() } else { $item.VersionInfo.FileVersion } VersionObject = $versionObject LastWriteTime = $item.LastWriteTimeUtc Source = 'cache' Action = 'SelectedCache' DownloadUrl = $info.DownloadUrl } } function Get-InstalledVCRuntime { <# .SYNOPSIS Detects the installed Microsoft VC runtime from the registry. .DESCRIPTION Checks the 32-bit and 64-bit Visual C++ runtime registry keys and returns a normalized installation record for the x64 redistributable. .EXAMPLE Get-InstalledVCRuntime #> [CmdletBinding()] param() $subKeyPaths = @( 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64' ) $views = @([Microsoft.Win32.RegistryView]::Registry64, [Microsoft.Win32.RegistryView]::Registry32) | Select-Object -Unique foreach ($view in $views) { $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view) try { foreach ($subKeyPath in $subKeyPaths) { $subKey = $baseKey.OpenSubKey($subKeyPath) if (-not $subKey) { continue } try { $installed = [int]$subKey.GetValue('Installed', 0) $versionText = [string]$subKey.GetValue('Version', '') $versionObject = ConvertTo-VCRuntimeVersion -VersionText $versionText if (-not $versionObject) { $major = $subKey.GetValue('Major', $null) $minor = $subKey.GetValue('Minor', $null) $bld = $subKey.GetValue('Bld', $null) $rbld = $subKey.GetValue('Rbld', $null) if ($null -ne $major -and $null -ne $minor -and $null -ne $bld -and $null -ne $rbld) { $versionObject = [version]::new([int]$major, [int]$minor, [int]$bld, [int]$rbld) $versionText = $versionObject.ToString() } } if ($installed -eq 1) { return [pscustomobject]@{ Installed = $true Architecture = 'x64' Version = $versionText VersionObject = $versionObject KeyPath = $subKeyPath RegistryView = $view.ToString() } } } finally { $subKey.Dispose() } } } finally { $baseKey.Dispose() } } [pscustomobject]@{ Installed = $false Architecture = 'x64' Version = $null VersionObject = $null KeyPath = $null RegistryView = $null } } function Test-VCRuntime { <# .SYNOPSIS Normalizes installed VC runtime information into a readiness result. .DESCRIPTION Converts the raw installed-runtime record into the status object used by the bootstrap flow so callers can reason about Ready versus Missing state. .PARAMETER InstalledRuntime Registry-based installation record returned by Get-InstalledVCRuntime. .EXAMPLE Test-VCRuntime -InstalledRuntime (Get-InstalledVCRuntime) #> [CmdletBinding()] param( [pscustomobject]$InstalledRuntime = (Get-InstalledVCRuntime) ) $status = if ($InstalledRuntime.Installed) { 'Ready' } else { 'Missing' } [pscustomobject]@{ Status = $status Installed = $InstalledRuntime.Installed Architecture = $InstalledRuntime.Architecture Version = $InstalledRuntime.Version VersionObject = $InstalledRuntime.VersionObject KeyPath = $InstalledRuntime.KeyPath RegistryView = $InstalledRuntime.RegistryView } } function Get-VCRuntimeState { <# .SYNOPSIS Builds the current VC runtime state for the manifested sandbox. .DESCRIPTION Combines the cached installer state, installed redistributable state, and any partial download artifacts into the normalized runtime snapshot used by Initialize-VCRuntime. .PARAMETER LocalRoot Sandbox local root used to resolve installer cache paths. .EXAMPLE Get-VCRuntimeState #> [CmdletBinding()] param( [string]$LocalRoot = (Get-ManifestedLocalRoot) ) if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) { return [pscustomobject]@{ Status = 'Blocked' LocalRoot = $LocalRoot Layout = $null CurrentVersion = $null InstalledRuntime = $null Runtime = $null Installer = $null InstallerPath = $null PartialPaths = @() BlockedReason = 'Only Windows hosts are supported by this VC runtime bootstrap.' } } $layout = Get-ManifestedLayout -LocalRoot $LocalRoot $installerInfo = Get-VCRuntimeInstallerInfo -LocalRoot $layout.LocalRoot $partialPaths = @() $downloadPath = Get-ManifestedDownloadPath -TargetPath $installerInfo.CachePath if (Test-Path -LiteralPath $downloadPath) { $partialPaths += $downloadPath } $installedRuntime = Get-InstalledVCRuntime $runtime = Test-VCRuntime -InstalledRuntime $installedRuntime $installer = Get-CachedVCRuntimeInstaller -LocalRoot $layout.LocalRoot if ($partialPaths.Count -gt 0) { $status = 'Partial' } elseif ($runtime.Status -eq 'Ready') { $status = 'Ready' } elseif ($installer) { $status = 'NeedsInstall' } else { $status = 'Missing' } [pscustomobject]@{ Status = $status LocalRoot = $layout.LocalRoot Layout = $layout CurrentVersion = if ($runtime.Version) { $runtime.Version } elseif ($installer) { $installer.Version } else { $null } InstalledRuntime = $installedRuntime Runtime = $runtime Installer = $installer InstallerPath = if ($installer) { $installer.Path } else { $installerInfo.CachePath } PartialPaths = $partialPaths BlockedReason = $null } } function Repair-VCRuntime { <# .SYNOPSIS Removes partial or corrupt VC runtime installer artifacts. .DESCRIPTION Collects staged download remnants and any explicitly supplied corrupt installer paths, removes them, and returns a repair summary for the runtime flow. .PARAMETER State Existing VC runtime state to repair. When omitted, the current state is loaded. .PARAMETER CorruptInstallerPaths Additional installer paths to remove during the repair pass. .PARAMETER LocalRoot Sandbox local root used when state must be rediscovered. .EXAMPLE Repair-VCRuntime -State (Get-VCRuntimeState) #> [CmdletBinding()] param( [pscustomobject]$State, [string[]]$CorruptInstallerPaths = @(), [string]$LocalRoot = (Get-ManifestedLocalRoot) ) if (-not $State) { $State = Get-VCRuntimeState -LocalRoot $LocalRoot } $pathsToRemove = New-Object System.Collections.Generic.List[string] foreach ($path in @($State.PartialPaths)) { if (-not [string]::IsNullOrWhiteSpace($path)) { $pathsToRemove.Add($path) | Out-Null } } foreach ($path in @($CorruptInstallerPaths)) { if (-not [string]::IsNullOrWhiteSpace($path)) { $pathsToRemove.Add($path) | Out-Null } } $removedPaths = New-Object System.Collections.Generic.List[string] foreach ($path in ($pathsToRemove | Select-Object -Unique)) { if (Remove-ManifestedPath -Path $path) { $removedPaths.Add($path) | Out-Null } } [pscustomobject]@{ Action = if ($removedPaths.Count -gt 0) { 'Repaired' } else { 'Skipped' } RemovedPaths = @($removedPaths) LocalRoot = $State.LocalRoot Layout = $State.Layout } } function Save-VCRuntimeInstaller { <# .SYNOPSIS Ensures the VC runtime bootstrapper is available in the managed cache. .DESCRIPTION Downloads the Microsoft VC++ redistributable bootstrapper when needed, falls back to the cached copy on refresh failures, and returns normalized installer metadata for the selected cache entry. .PARAMETER RefreshVCRuntime Forces the installer to be re-downloaded instead of reusing the cached copy. .PARAMETER LocalRoot Sandbox local root used to resolve cache locations. .EXAMPLE Save-VCRuntimeInstaller .EXAMPLE Save-VCRuntimeInstaller -RefreshVCRuntime #> [CmdletBinding()] param( [switch]$RefreshVCRuntime, [string]$LocalRoot = (Get-ManifestedLocalRoot) ) $layout = Get-ManifestedLayout -LocalRoot $LocalRoot $info = Get-VCRuntimeInstallerInfo -LocalRoot $layout.LocalRoot New-ManifestedDirectory -Path $layout.VCRuntimeCacheRoot | Out-Null $cacheExists = Test-Path -LiteralPath $info.CachePath $downloadPath = Get-ManifestedDownloadPath -TargetPath $info.CachePath $action = 'ReusedCache' if ($RefreshVCRuntime -or -not $cacheExists) { Remove-ManifestedPath -Path $downloadPath | Out-Null try { Write-Host 'Downloading Microsoft Visual C++ Redistributable bootstrapper...' Invoke-WebRequestEx -Uri $info.DownloadUrl -OutFile $downloadPath -UseBasicParsing Move-Item -LiteralPath $downloadPath -Destination $info.CachePath -Force $action = 'Downloaded' } catch { Remove-ManifestedPath -Path $downloadPath | Out-Null if (-not $cacheExists) { throw } Write-Warning ('Could not refresh the VC++ redistributable bootstrapper. Using cached copy. ' + $_.Exception.Message) $action = 'ReusedCache' } } if (-not (Test-Path -LiteralPath $info.CachePath)) { throw 'Could not acquire the VC++ redistributable bootstrapper and no cached copy was found.' } $cached = Get-CachedVCRuntimeInstaller -LocalRoot $layout.LocalRoot [pscustomobject]@{ Architecture = $info.Architecture FileName = $info.FileName DownloadUrl = $info.DownloadUrl Path = $info.CachePath Version = if ($cached) { $cached.Version } else { $null } VersionObject = if ($cached) { $cached.VersionObject } else { $null } Source = if ($action -eq 'Downloaded') { 'online' } else { 'cache' } Action = $action } } function Test-VCRuntimeInstaller { <# .SYNOPSIS Validates a cached VC runtime installer. .DESCRIPTION Verifies that the cached installer exists and is authenticode-signed by Microsoft so the runtime flow can distinguish ready cache entries from corrupt ones. .PARAMETER InstallerInfo Installer metadata returned by Save-VCRuntimeInstaller or Get-CachedVCRuntimeInstaller. .EXAMPLE Test-VCRuntimeInstaller -InstallerInfo (Save-VCRuntimeInstaller) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$InstallerInfo ) if (-not (Test-Path -LiteralPath $InstallerInfo.Path)) { return [pscustomobject]@{ Status = 'Missing' Architecture = $InstallerInfo.Architecture Path = $InstallerInfo.Path Version = $InstallerInfo.Version VersionObject = $InstallerInfo.VersionObject SignatureStatus = 'Missing' SignerSubject = $null } } $signature = Get-AuthenticodeSignature -FilePath $InstallerInfo.Path $status = 'Ready' if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { $status = 'CorruptCache' } elseif (-not $signature.SignerCertificate -or $signature.SignerCertificate.Subject -notmatch 'Microsoft Corporation') { $status = 'CorruptCache' } [pscustomobject]@{ Status = $status Architecture = $InstallerInfo.Architecture Path = $InstallerInfo.Path Version = $InstallerInfo.Version VersionObject = $InstallerInfo.VersionObject SignatureStatus = $signature.Status.ToString() SignerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { $null } } } function Invoke-VCRuntimeInstaller { <# .SYNOPSIS Runs the VC runtime bootstrapper in quiet mode. .DESCRIPTION Starts the redistributable installer with silent arguments, waits for it to finish, captures the generated log path, and enforces a caller-supplied timeout. .PARAMETER InstallerPath Path to the VC runtime bootstrapper executable to launch. .PARAMETER TimeoutSec Maximum number of seconds to wait before terminating the installer. .PARAMETER LocalRoot Sandbox local root used to place the installer log file. .EXAMPLE Invoke-VCRuntimeInstaller -InstallerPath $installer.Path #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$InstallerPath, [int]$TimeoutSec = 300, [string]$LocalRoot = (Get-ManifestedLocalRoot) ) $layout = Get-ManifestedLayout -LocalRoot $LocalRoot New-ManifestedDirectory -Path $layout.VCRuntimeCacheRoot | Out-Null $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' $logPath = Join-Path $layout.VCRuntimeCacheRoot ("vc_redist.install.$timestamp.log") $argumentList = @( '/install', '/quiet', '/norestart', '/log', (Format-VCRuntimeProcessArgument -Value $logPath) ) $process = Start-Process -FilePath $InstallerPath -ArgumentList $argumentList -PassThru if (-not $process.WaitForExit($TimeoutSec * 1000)) { try { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } catch { } throw "VC++ redistributable installation exceeded the timeout of $TimeoutSec seconds. Check $logPath." } [pscustomobject]@{ ExitCode = $process.ExitCode LogPath = $logPath RestartRequired = ($process.ExitCode -eq 3010) } } function Install-VCRuntime { <# .SYNOPSIS Installs the VC runtime when the current machine is missing or behind. .DESCRIPTION Compares the installed VC++ redistributable with the cached installer version, skips installation when the machine is already up to date, and otherwise runs the installer and validates the result. .PARAMETER InstallerInfo Validated installer metadata for the VC runtime bootstrapper. .PARAMETER InstallTimeoutSec Maximum number of seconds to wait for the installer process. .PARAMETER LocalRoot Sandbox local root used for installer logging and cache resolution. .EXAMPLE Install-VCRuntime -InstallerInfo (Save-VCRuntimeInstaller) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$InstallerInfo, [int]$InstallTimeoutSec = 300, [string]$LocalRoot = (Get-ManifestedLocalRoot) ) $installed = Get-InstalledVCRuntime if ($installed.Installed) { if (-not $InstallerInfo.VersionObject -or ($installed.VersionObject -and $installed.VersionObject -ge $InstallerInfo.VersionObject)) { return [pscustomobject]@{ Action = 'Skipped' Installed = $true Architecture = $installed.Architecture Version = $installed.Version VersionObject = $installed.VersionObject InstallerVersion = $InstallerInfo.Version InstallerPath = $InstallerInfo.Path InstallerSource = $InstallerInfo.Source ExitCode = 0 RestartRequired = $false LogPath = $null } } } Write-Host 'Installing Microsoft Visual C++ Redistributable prerequisites for the runtime...' $installResult = Invoke-VCRuntimeInstaller -InstallerPath $InstallerInfo.Path -TimeoutSec $InstallTimeoutSec -LocalRoot $LocalRoot $refreshed = Get-InstalledVCRuntime if (-not $refreshed.Installed) { throw "VC++ redistributable installation exited with code $($installResult.ExitCode), but the runtime was not detected afterwards. Check $($installResult.LogPath)." } if ($InstallerInfo.VersionObject -and $refreshed.VersionObject -and $refreshed.VersionObject -lt $InstallerInfo.VersionObject) { throw "VC++ redistributable installation completed, but version $($refreshed.Version) is still older than the cached installer version $($InstallerInfo.Version). Check $($installResult.LogPath)." } if ($installResult.ExitCode -notin @(0, 3010, 1638)) { throw "VC++ redistributable installation failed with exit code $($installResult.ExitCode). Check $($installResult.LogPath)." } [pscustomobject]@{ Action = 'Installed' Installed = $true Architecture = $refreshed.Architecture Version = $refreshed.Version VersionObject = $refreshed.VersionObject InstallerVersion = $InstallerInfo.Version InstallerPath = $InstallerInfo.Path InstallerSource = $InstallerInfo.Source ExitCode = $installResult.ExitCode RestartRequired = $installResult.RestartRequired LogPath = $installResult.LogPath } } |