Eigenverft.Manifested.Sandbox.Cmd.VCRuntimeAndCache.ps1
|
<#
Eigenverft.Manifested.Sandbox.Cmd.VCRuntimeAndCache #> function ConvertTo-VCRuntimeVersion { [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 { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Value ) if ($Value.IndexOfAny([char[]]@(' ', "`t", '"')) -ge 0) { return '"' + ($Value -replace '"', '\"') + '"' } return $Value } function 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 { [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 { [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 { [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 { [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 { [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 { [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-WebRequest -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 { [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 { [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 { [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 } } function Initialize-VCRuntime { [CmdletBinding(SupportsShouldProcess = $true)] param( [switch]$RefreshVCRuntime, [int]$InstallTimeoutSec = 300 ) $LocalRoot = (Get-ManifestedLayout).LocalRoot $selfElevationContext = Get-ManifestedSelfElevationContext $actionsTaken = New-Object System.Collections.Generic.List[string] $plannedActions = New-Object System.Collections.Generic.List[string] $repairResult = $null $installerInfo = $null $installerTest = $null $installResult = $null $initialState = Get-VCRuntimeState -LocalRoot $LocalRoot $state = $initialState $elevationPlan = Get-ManifestedCommandElevationPlan -CommandName 'Initialize-VCRuntime' -LocalRoot $LocalRoot -SkipSelfElevation:$selfElevationContext.SkipSelfElevation -WasSelfElevated:$selfElevationContext.WasSelfElevated -WhatIfMode:$WhatIfPreference if ($state.Status -eq 'Blocked') { $result = [pscustomobject]@{ LocalRoot = $state.LocalRoot Layout = $state.Layout InitialState = $initialState FinalState = $state ActionTaken = @('None') PlannedActions = @() RestartRequired = $false Installer = $null InstallerTest = $null RuntimeTest = $null RepairResult = $null InstallResult = $null Elevation = $elevationPlan } if ($WhatIfPreference) { Add-Member -InputObject $result -NotePropertyName PersistedStatePath -NotePropertyValue $null -Force return $result } $statePath = Save-ManifestedInvokeState -CommandName 'Initialize-VCRuntime' -Result $result -LocalRoot $LocalRoot -Details @{ Version = $state.CurrentVersion InstallerPath = $state.InstallerPath } Add-Member -InputObject $result -NotePropertyName PersistedStatePath -NotePropertyValue $statePath -Force return $result } $needsRepair = $state.Status -in @('Partial', 'NeedsRepair') $needsInstall = $RefreshVCRuntime -or ($state.Status -ne 'Ready') $needsAcquire = $RefreshVCRuntime -or (-not $state.InstallerPath) -or (-not (Test-Path -LiteralPath $state.InstallerPath)) if ($needsRepair) { $plannedActions.Add('Repair-VCRuntime') | Out-Null } if ($needsInstall -and $needsAcquire) { $plannedActions.Add('Save-VCRuntimeInstaller') | Out-Null } if ($needsInstall) { $plannedActions.Add('Test-VCRuntimeInstaller') | Out-Null $plannedActions.Add('Install-VCRuntime') | Out-Null } $elevationPlan = Get-ManifestedCommandElevationPlan -CommandName 'Initialize-VCRuntime' -PlannedActions @($plannedActions) -LocalRoot $LocalRoot -SkipSelfElevation:$selfElevationContext.SkipSelfElevation -WasSelfElevated:$selfElevationContext.WasSelfElevated -WhatIfMode:$WhatIfPreference if ($needsRepair) { if (-not $PSCmdlet.ShouldProcess($state.InstallerPath, 'Repair VC runtime state')) { return [pscustomobject]@{ LocalRoot = $state.LocalRoot Layout = $state.Layout InitialState = $initialState FinalState = $state ActionTaken = @('WhatIf') PlannedActions = @($plannedActions) RestartRequired = $false Installer = $null InstallerTest = $null RuntimeTest = $state.Runtime RepairResult = $null InstallResult = $null PersistedStatePath = $null Elevation = $elevationPlan } } $repairResult = Repair-VCRuntime -State $state -LocalRoot $state.LocalRoot if ($repairResult.Action -eq 'Repaired') { $actionsTaken.Add('Repair-VCRuntime') | Out-Null } $state = Get-VCRuntimeState -LocalRoot $state.LocalRoot $needsInstall = $RefreshVCRuntime -or ($state.Status -ne 'Ready') $needsAcquire = $RefreshVCRuntime -or (-not $state.InstallerPath) -or (-not (Test-Path -LiteralPath $state.InstallerPath)) } if ($needsInstall) { if ($needsAcquire) { if (-not $PSCmdlet.ShouldProcess($state.Layout.VCRuntimeCacheRoot, 'Acquire VC runtime installer')) { return [pscustomobject]@{ LocalRoot = $state.LocalRoot Layout = $state.Layout InitialState = $initialState FinalState = $state ActionTaken = @('WhatIf') PlannedActions = @($plannedActions) RestartRequired = $false Installer = $null InstallerTest = $null RuntimeTest = $state.Runtime RepairResult = $repairResult InstallResult = $null PersistedStatePath = $null Elevation = $elevationPlan } } $installerInfo = Save-VCRuntimeInstaller -RefreshVCRuntime:$RefreshVCRuntime -LocalRoot $state.LocalRoot if ($installerInfo.Action -eq 'Downloaded') { $actionsTaken.Add('Save-VCRuntimeInstaller') | Out-Null } } else { $installerInfo = $state.Installer } $installerTest = Test-VCRuntimeInstaller -InstallerInfo $installerInfo if ($installerTest.Status -eq 'CorruptCache') { if (-not $PSCmdlet.ShouldProcess($installerInfo.Path, 'Repair corrupt VC runtime installer')) { return [pscustomobject]@{ LocalRoot = $state.LocalRoot Layout = $state.Layout InitialState = $initialState FinalState = $state ActionTaken = @('WhatIf') PlannedActions = @($plannedActions) RestartRequired = $false Installer = $installerInfo InstallerTest = $installerTest RuntimeTest = $state.Runtime RepairResult = $repairResult InstallResult = $null PersistedStatePath = $null Elevation = $elevationPlan } } $repairResult = Repair-VCRuntime -State $state -CorruptInstallerPaths @($installerInfo.Path) -LocalRoot $state.LocalRoot if ($repairResult.Action -eq 'Repaired') { $actionsTaken.Add('Repair-VCRuntime') | Out-Null } $installerInfo = Save-VCRuntimeInstaller -RefreshVCRuntime:$true -LocalRoot $state.LocalRoot if ($installerInfo.Action -eq 'Downloaded') { $actionsTaken.Add('Save-VCRuntimeInstaller') | Out-Null } $installerTest = Test-VCRuntimeInstaller -InstallerInfo $installerInfo } if ($installerTest.Status -ne 'Ready') { throw "VC runtime installer validation failed with status $($installerTest.Status)." } $elevationPlan = Get-ManifestedCommandElevationPlan -CommandName 'Initialize-VCRuntime' -PlannedActions @($plannedActions) -Context @{ InstalledRuntime = $state.InstalledRuntime InstallerInfo = $installerInfo } -LocalRoot $state.LocalRoot -SkipSelfElevation:$selfElevationContext.SkipSelfElevation -WasSelfElevated:$selfElevationContext.WasSelfElevated -WhatIfMode:$WhatIfPreference $commandParameters = @{ InstallTimeoutSec = $InstallTimeoutSec } if ($RefreshVCRuntime) { $commandParameters['RefreshVCRuntime'] = $true } if ($PSBoundParameters.ContainsKey('WhatIf')) { $commandParameters['WhatIf'] = $true } $elevatedResult = Invoke-ManifestedElevatedCommand -ElevationPlan $elevationPlan -CommandName 'Initialize-VCRuntime' -CommandParameters $commandParameters if ($null -ne $elevatedResult) { return $elevatedResult } if (-not $PSCmdlet.ShouldProcess('Microsoft Visual C++ Redistributable (x64)', 'Install VC runtime')) { return [pscustomobject]@{ LocalRoot = $state.LocalRoot Layout = $state.Layout InitialState = $initialState FinalState = $state ActionTaken = @('WhatIf') PlannedActions = @($plannedActions) RestartRequired = $false Installer = $installerInfo InstallerTest = $installerTest RuntimeTest = $state.Runtime RepairResult = $repairResult InstallResult = $null PersistedStatePath = $null Elevation = $elevationPlan } } $installResult = Install-VCRuntime -InstallerInfo $installerInfo -InstallTimeoutSec $InstallTimeoutSec -LocalRoot $state.LocalRoot if ($installResult.Action -eq 'Installed') { $actionsTaken.Add('Install-VCRuntime') | Out-Null } } $finalState = Get-VCRuntimeState -LocalRoot $state.LocalRoot $runtimeTest = Test-VCRuntime -InstalledRuntime $finalState.InstalledRuntime $result = [pscustomobject]@{ LocalRoot = $finalState.LocalRoot Layout = $finalState.Layout InitialState = $initialState FinalState = $finalState ActionTaken = if ($actionsTaken.Count -gt 0) { @($actionsTaken) } else { @('None') } PlannedActions = @($plannedActions) RestartRequired = if ($installResult) { [bool]$installResult.RestartRequired } else { $false } Installer = $installerInfo InstallerTest = $installerTest RuntimeTest = $runtimeTest RepairResult = $repairResult InstallResult = $installResult Elevation = $elevationPlan } if ($WhatIfPreference) { Add-Member -InputObject $result -NotePropertyName PersistedStatePath -NotePropertyValue $null -Force return $result } $statePath = Save-ManifestedInvokeState -CommandName 'Initialize-VCRuntime' -Result $result -LocalRoot $LocalRoot -Details @{ Version = $finalState.CurrentVersion InstallerPath = $finalState.InstallerPath } Add-Member -InputObject $result -NotePropertyName PersistedStatePath -NotePropertyValue $statePath -Force return $result } |