PSModuleUpdate.psm1
|
Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # Source: src/public/Get-PSModuleUpdate.ps1 function Get-PSModuleUpdate { <# .EXTERNALHELP PSModuleUpdate-Help.xml .SYNOPSIS Creates an exact-version update plan without changing installed PowerShell modules. .DESCRIPTION Inspects a compatible installed module in CurrentUser or AllUsers and one exact repository. The target need not be loaded, be the caller, or depend on PSModuleUpdate. Policy defaults to Stable and CurrentUser. Credentials are used only in memory. An unregistered URI uses an ephemeral untrusted registration and nonsecret ownership receipt. Cleanup validates exact registration identity. An interrupted unbound receipt fails closed. .PARAMETER Module One installed module reference. Name and GUID must match its on-disk manifest. .PARAMETER Repository One literal registered PSResourceGet repository name. .PARAMETER RepositoryUri Exact endpoint. Reuses one matching registration or creates an owned temporary transport. .PARAMETER Policy Channel, Scope, ExpectedRepositoryUri (Uri), and VersionRange. Unknown keys are rejected. .PARAMETER Credential Optional per-call provider credential. Supply it again when installing. .EXAMPLE Get-PSModuleUpdate -Module (Get-Module ExampleModule) -Repository PSGallery Creates a stable CurrentUser update plan without changing installed files. .EXAMPLE Get-PSModuleUpdate -Module $installed -RepositoryUri ([uri]'https://api.nuget.org/v3/index.json') Uses only the specified endpoint. Packages must contain a valid PowerShell module manifest. .OUTPUTS PSModuleUpdate.UpdatePlan #> [CmdletBinding(DefaultParameterSetName = 'ByModule')] param( [Parameter(Mandatory)][System.Management.Automation.PSModuleInfo]$Module, [Parameter(Mandatory, ParameterSetName = 'ByModule')][ValidateNotNullOrEmpty()][string]$Repository, [Parameter(Mandatory, ParameterSetName = 'ByRepositoryUri')][ValidateNotNull()][uri]$RepositoryUri, [hashtable]$Policy = @{}, [pscredential]$Credential ) $selection = @{ Name = $Repository; Uri = $RepositoryUri } $context = Get-PlanningContext $Module $selection $Policy $Credential Register-UpdatePlan (ConvertTo-UpdatePlan $context) $context ([bool]$Credential) } # Source: src/public/Install-PSModuleUpdate.ps1 function Install-PSModuleUpdate { <# .EXTERNALHELP PSModuleUpdate-Help.xml .SYNOPSIS Applies one live exact-version module update plan. .DESCRIPTION Revalidates and stages the exact package without importing it. Same-base replacement uses a verified backup, durable journal, exclusive OS handle lock, and best-effort rollback. Only provider-generated root PSGetModuleInfo.xml is excluded from SHA256 tree comparison. No module is hot reloaded. Interrupted operations require safe recovery and a fresh plan. .PARAMETER Update An unmodified live PSModuleUpdate.UpdatePlan from this module instance; pipeline by value. .PARAMETER Credential Per-call provider credential; never persisted. .PARAMETER AcceptLicense Explicitly accepts provider package license requirements for this operation. .PARAMETER TrustRepository Allows this operation against an untrusted repository without changing its registration. .EXAMPLE $plan | Install-PSModuleUpdate -WhatIf Revalidates read-only without locking, staging, installing, or repairing recovery state. .EXAMPLE $plan | Install-PSModuleUpdate -AcceptLicense -TrustRepository Requests native confirmation before applying the exact approved target and scope. .OUTPUTS PSModuleUpdate.UpdateResult #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High', DefaultParameterSetName = 'ByUpdatePlan')] param( [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ByUpdatePlan')][ValidateNotNull()][object]$Update, [Parameter(ParameterSetName = 'ByUpdatePlan')][pscredential]$Credential, [Parameter(ParameterSetName = 'ByUpdatePlan')][switch]$AcceptLicense, [Parameter(ParameterSetName = 'ByUpdatePlan')][switch]$TrustRepository ) process { $operation = Initialize-InstallationRequest $Update $Credential @{ AcceptLicense = [bool]$AcceptLicense; TrustRepository = [bool]$TrustRepository } $target = "$($Update.ModuleName) $($Update.TargetVersion) from $($Update.RepositoryName ?? $Update.RepositoryUri) into $($Update.Scope)" if ($PSCmdlet.ShouldProcess($target, 'Install exact module update')) { Invoke-UpdateInstallation $operation } else { ConvertTo-DeclinedUpdateResult $Update } } } # Source: src/private/identity/Assert-ManifestRootModule.ps1 function Assert-ManifestRootModule { param([hashtable]$Data, [string]$ManifestPath) if (-not $Data['RootModule']) { return } $root = Split-Path -Parent $ManifestPath $path = Assert-SafePath (Join-Path $root $Data['RootModule']) $relative = [IO.Path]::GetRelativePath($root, $path) if ($relative.StartsWith('..') -or [IO.Path]::IsPathRooted($relative)) { Write-PMUError 'PMU.ModuleIdentityMismatch' 'The declared root module must remain inside the package directory.' } if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Write-PMUError 'PMU.ModuleIdentityMismatch' 'The declared root module is missing from the extracted package.' } } # Source: src/private/identity/Assert-ReferenceInstallationLayout.ps1 function Assert-ReferenceInstallationLayout { param([object]$Reference, [object[]]$Observed, [object]$RecoveryJournal) $installations = @($Observed | Where-Object ManifestPath -CEQ $Reference.ManifestPath) if ($installations.Count -eq 1) { return } Assert-BackupReferenceIdentity $Reference $RecoveryJournal } # Source: src/private/identity/Assert-SelectedInstallation.ps1 function Assert-SelectedInstallation { param([object[]]$Installations, [guid]$Guid) if (-not $Installations.Count) { Write-PMUError 'PMU.UnsupportedInstallation' 'No existing installation was found in the requested scope.' } if (@($Installations | Where-Object Guid -NE $Guid).Count) { Write-PMUError 'PMU.GuidMismatch' 'A selected-scope manifest has a different GUID.' } } # Source: src/private/identity/Get-ActiveModuleIdentity.ps1 function Get-ActiveModuleIdentity { param([System.Management.Automation.PSModuleInfo]$Module) $loaded = @(Get-Module -Name $Module.Name | Where-Object Guid -EQ $Module.Guid | Select-Object -First 1) if (-not $loaded.Count) { return $null } $active = $loaded[0] [pscustomobject]@{ Name = $active.Name; Guid = $active.Guid; Version = Get-ActiveModuleVersion $active ManifestPath = Assert-SafePath (Join-Path $active.ModuleBase "$($active.Name).psd1") } } # Source: src/private/identity/Get-ActiveModuleVersion.ps1 function Get-ActiveModuleVersion { param([System.Management.Automation.PSModuleInfo]$Module) $full = "$($Module.Version)" $label = '' if ($Module.PrivateData -is [System.Collections.IDictionary]) { $privateData = $Module.PrivateData if ($privateData['PSData']) { $label = "$($privateData['PSData']['Prerelease'])" } } if ($label) { $full += "-$label" } ConvertTo-CompleteModuleVersion $full } # Source: src/private/identity/Get-InstalledModuleIdentityList.ps1 function Get-InstalledModuleIdentityList { param([string]$Name, [string]$Scope, [string]$ExcludedPath) $root = Resolve-ModuleInstallationRoot $Scope $directory = Assert-SafePath (Join-Path $root $Name) if (-not (Test-Path -LiteralPath $directory)) { return } foreach ($child in Get-ChildItem -LiteralPath $directory -Directory -Force) { if ($child.FullName -ceq $ExcludedPath) { continue } $identity = Get-ManifestIdentity (Join-Path $child.FullName "$Name.psd1") if ($child.Name -cne $identity.Version.NumericVersion) { Write-PMUError 'PMU.UnsupportedInstallation' 'Only provider numeric-version directory layouts are supported.' } [pscustomobject]@{ FullVersion = $identity.Version.FullVersion; NumericVersion = $identity.Version.NumericVersion; Prerelease = $identity.Version.Prerelease; Scope = $Scope; Root = $root; ManifestPath = $identity.ManifestPath; Guid = $identity.Guid } } } # Source: src/private/identity/Get-ManifestIdentity.ps1 function Get-ManifestIdentity { param([string]$Path) $safe = Assert-SafePath $Path try { $data = Import-PowerShellDataFile -LiteralPath $safe -ErrorAction Stop } catch { Write-PMUError 'PMU.ModuleIdentityMismatch' 'The module manifest cannot be safely read.' } if (-not $data['GUID'] -or -not $data['ModuleVersion']) { Write-PMUError 'PMU.ModuleIdentityMismatch' 'The module manifest must declare version and GUID.' } Assert-ManifestRootModule $data $safe $version = Get-ManifestModuleVersion $data [pscustomobject]@{ Name = [IO.Path]::GetFileNameWithoutExtension($safe); Guid = [guid]$data.GUID; Version = $version; ManifestPath = $safe } } # Source: src/private/identity/Get-ManifestModuleVersion.ps1 function Get-ManifestModuleVersion { param([hashtable]$Data) $label = '' if ($Data['PrivateData'] -and $Data['PrivateData']['PSData']) { $label = "$($Data['PrivateData']['PSData']['Prerelease'])" } $full = "$($Data['ModuleVersion'])" if ($label) { $full += "-$label" } ConvertTo-CompleteModuleVersion $full } # Source: src/private/identity/Get-ModuleReferenceIdentity.ps1 function Get-ModuleReferenceIdentity { param([System.Management.Automation.PSModuleInfo]$Module) if ($Module.Name -notmatch '^[A-Za-z0-9][A-Za-z0-9_.-]*$') { Write-PMUError 'PMU.InvalidRequest' 'A literal module identity is required.' } $identity = Get-ManifestIdentity (Join-Path $Module.ModuleBase "$($Module.Name).psd1") if ($identity.Guid -ne $Module.Guid -or [version]$identity.Version.NumericVersion -ne $Module.Version) { Write-PMUError 'PMU.ModuleIdentityMismatch' 'The supplied module reference differs from its manifest.' } $identity.Version = Get-ActiveModuleVersion $Module $identity } # Source: src/private/identity/Resolve-ModuleInstallationRoot.ps1 function Resolve-ModuleInstallationRoot { param([ValidateSet('CurrentUser', 'AllUsers')][string]$Scope) if ($IsWindows) { if ($Scope -eq 'AllUsers') { return Assert-SafePath (Join-Path $env:ProgramFiles 'PowerShell/Modules') } return Assert-SafePath (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell/Modules') } if ($Scope -eq 'AllUsers') { return Assert-SafePath '/usr/local/share/powershell/Modules' } Assert-SafePath (Join-Path $HOME '.local/share/powershell/Modules') } # Source: src/private/installation/Assert-ActivePlanIdentity.ps1 function Assert-ActivePlanIdentity { param([object]$Operation) $module = $Operation.Record.Module $path = Assert-SafePath (Join-Path $module.ModuleBase "$($module.Name).psd1") if ($path -cne $Operation.Record.ReferencePath -or $module.Guid -ne $Operation.Plan.ModuleGuid) { Write-PMUError 'PMU.PlanStale' 'The original active module identity or path changed.' } Assert-LoadedPlanIdentity $Operation } # Source: src/private/installation/Assert-ExactTarget.ps1 function Assert-ExactTarget { param([object]$Operation) $plan = $Operation.Plan Assert-RepositoryBinding $plan $Operation.RepositoryName $request = @{ Name = $plan.ModuleName; Repository = Get-OperationRepositoryName $Operation; Version = "[$($plan.TargetVersion)]"; Prerelease = $plan.TargetIsPrerelease } $resources = @(Find-ProviderResource $request $Operation.Credential | ForEach-Object { ConvertTo-ProviderRelease $_ }) $exact = @($resources | Where-Object { $_.Name -ceq $plan.ModuleName -and $_.Version.FullVersion -ceq $plan.TargetVersion }) if ($exact.Count -ne 1) { Write-PMUError 'PMU.PackageNotFound' 'The exact approved target is no longer uniquely available.' } $Operation.RequiresLicenseAcceptance = $exact[0].RequiresLicenseAcceptance } # Source: src/private/installation/Assert-InstallationConsent.ps1 function Assert-InstallationConsent { param([object]$Operation) Assert-RequiredConsent $Operation.Record.RequiresCredential ([bool]$Operation.Credential) 'PMU.AuthenticationFailed' 'Supply the direct credential again; PMU does not retain credentials between calls.' Assert-RequiredConsent $Operation.Plan.RequiresRepositoryTrust $Operation.Consent.TrustRepository 'PMU.RepositoryTrustRequired' 'Explicit -TrustRepository is required for this operation.' Assert-RequiredConsent ($Operation.Plan.RequiresLicenseAcceptance -or $Operation.RequiresLicenseAcceptance) $Operation.Consent.AcceptLicense 'PMU.LicenseAcceptanceRequired' 'Explicit -AcceptLicense is required when package license requirements are declared or undisclosed.' } # Source: src/private/installation/Assert-LoadedPlanIdentity.ps1 function Assert-LoadedPlanIdentity { param([object]$Operation) $active = Get-ActiveModuleIdentity $Operation.Record.Module $actual = @{ ActivePath = $null; ActiveVersion = $null } if ($active) { $actual = @{ ActivePath = $active.ManifestPath; ActiveVersion = $active.Version.FullVersion } } Assert-MatchingProperty $actual $Operation.Plan @('ActivePath', 'ActiveVersion') 'PMU.PlanStale' } # Source: src/private/installation/Assert-PlanEnvironment.ps1 function Assert-PlanEnvironment { param([object]$Operation) $plan = $Operation.Plan $provider = Get-ProviderContext Assert-RepositoryBinding $plan $Operation.RepositoryName $root = Resolve-ModuleInstallationRoot $plan.Scope if ($root -cne $plan.DestinationRoot -or "$($provider.Version)" -cne $plan.ProviderVersion) { Write-PMUError 'PMU.PlanStale' 'Destination or provider context changed after planning.' } Assert-ActivePlanIdentity $Operation } # Source: src/private/installation/Assert-RequiredConsent.ps1 function Assert-RequiredConsent { param([bool]$Required, [bool]$Allowed, [string]$ErrorId, [string]$Message) if ($Required -and -not $Allowed) { Write-PMUError $ErrorId $Message } } # Source: src/private/installation/Complete-VerifiedInstallation.ps1 function Complete-VerifiedInstallation { param([object]$Operation, [object[]]$Tree, [bool]$AlreadyInstalled) $outcome = 'AlreadyInstalled' if ($AlreadyInstalled) { Assert-InstalledPackage $Operation $Tree } else { if (-not $Operation.Plan.UpdateAvailable) { Write-PMUError 'PMU.PlanStale' 'This plan does not authorize a newer target installation.' } Initialize-Replacement $Operation $Tree Invoke-ProviderInstallation $Operation $Tree $outcome = 'Installed' } $result = ConvertTo-UpdateResult $Operation.Plan $outcome 'Verified' $result.RecoveryStatus = $Operation.RecoveryStatus Clear-CompletedOperation $Operation $result } # Source: src/private/installation/ConvertTo-DeclinedUpdateResult.ps1 function ConvertTo-DeclinedUpdateResult { param([object]$Plan) $outcome = 'Cancelled' if ($WhatIfPreference) { $outcome = 'WhatIf' } ConvertTo-UpdateResult $Plan $outcome } # Source: src/private/installation/ConvertTo-InstallationOperation.ps1 function ConvertTo-InstallationOperation { param([object]$Plan, [object]$Record, [hashtable]$Consent, [pscredential]$Credential) $dataRoot = Resolve-PMUDataRoot $Plan.Scope $work = Join-Path $dataRoot (Get-InstallationLockIdentity $Plan) [pscustomobject]@{ Plan = $Plan; Record = $Record; Consent = $Consent; Credential = $Credential RepositoryName = $null DataRoot = $dataRoot; WorkRoot = $work; LockPath = Join-Path $work 'operation.lock' JournalPath = Join-Path $work 'journal.json'; StageRoot = Join-Path $work 'stage' BackupPath = Join-Path $work 'backup'; Reinstall = $false; Journal = $null StagedPath = Join-Path $work "stage/$($Plan.ModuleName)/$($Plan.TargetNumericVersion)" RecoveryStatus = 'NotRequired' RequiresLicenseAcceptance = $false } } # Source: src/private/installation/ConvertTo-UpdateResult.ps1 function ConvertTo-UpdateResult { param([object]$Plan, [string]$Outcome, [string]$VerificationStatus = 'NotPerformed') $installed = Get-ResultInstallationState $Plan $VerificationStatus [pscustomobject]@{ PSTypeName = 'PSModuleUpdate.UpdateResult'; SchemaVersion = 1; Outcome = $Outcome ModuleName = $Plan.ModuleName; ModuleGuid = $Plan.ModuleGuid; RepositoryName = $Plan.RepositoryName RepositoryUri = $Plan.RepositoryUri; Scope = $Plan.Scope; ActiveVersion = $Plan.ActiveVersion InstalledVersion = $installed.Version; TargetVersion = $Plan.TargetVersion; InstalledPath = $installed.Path VerificationStatus = $VerificationStatus; RestartRequired = (Test-RestartRequired $Plan $Outcome) DependencySummary = @($Plan.DependencySummary); RecoveryStatus = 'NotRequired'; RecoveryGuidance = '' } } # Source: src/private/installation/Get-ResultInstallationState.ps1 function Get-ResultInstallationState { param([object]$Plan, [string]$VerificationStatus) if ($VerificationStatus -ne 'Verified') { return @{ Version = $Plan.InstalledVersion; Path = Split-Path -Parent $Plan.InstalledManifestPath } } $identity = Get-ManifestIdentity (Join-Path $Plan.TargetInstalledPath "$($Plan.ModuleName).psd1") @{ Version = $identity.Version.FullVersion; Path = $Plan.TargetInstalledPath } } # Source: src/private/installation/Initialize-InstallationRequest.ps1 function Initialize-InstallationRequest { param([object]$Update, [pscredential]$Credential, [hashtable]$Consent) $record = Test-UpdatePlan $Update $operation = ConvertTo-InstallationOperation $Update $record $Consent $Credential Assert-PlanEnvironment $operation if ($Update.RepositoryName) { Assert-ExactTarget $operation } $journal = Read-RecoveryJournal $operation if (-not $journal -or $journal.State -in @('Completed', 'RecoveryCompleted')) { $null = Test-InstalledPlanState $operation } $operation } # Source: src/private/installation/Initialize-Replacement.ps1 function Initialize-Replacement { param([object]$Operation, [object[]]$Tree) $path = $Operation.Plan.TargetInstalledPath $Operation.Reinstall = Test-Path -LiteralPath $path if ($Operation.Reinstall) { $identity = Get-ManifestIdentity (Join-Path $path "$($Operation.Plan.ModuleName).psd1") if ($identity.Guid -ne $Operation.Plan.ModuleGuid) { Write-PMUError 'PMU.GuidMismatch' 'Refusing to replace a different GUID.' } } $Operation.Journal = ConvertTo-RecoveryJournal $Operation $Tree Write-RecoveryJournalState $Operation.Journal 'Preparation' if ($Operation.Reinstall) { Copy-InstallationBackup $Operation } } # Source: src/private/installation/Invoke-LockedInstallation.ps1 function Invoke-LockedInstallation { param([object]$Operation) Assert-PlanEnvironment $Operation Repair-UnfinishedInstallation $Operation $alreadyInstalled = Test-InstalledPlanState $Operation Clear-CompletedOperation $Operation Assert-ExactTarget $Operation Assert-InstallationConsent $Operation $tree = @(Initialize-StagedPackage $Operation) Complete-VerifiedInstallation $Operation $tree $alreadyInstalled } # Source: src/private/installation/Invoke-ProviderInstallation.ps1 function Invoke-ProviderInstallation { param([object]$Operation, [object[]]$StagedTree) Write-RecoveryJournalState $Operation.Journal 'ReplacementInProgress' try { Write-InstallationPhase $Operation.Plan 'Replacing' Assert-RepositoryBinding $Operation.Plan $Operation.RepositoryName Install-ProviderResource $Operation Write-RecoveryJournalState $Operation.Journal 'Verification' Write-InstallationPhase $Operation.Plan 'Verifying' Assert-InstalledPackage $Operation $StagedTree Write-RecoveryJournalState $Operation.Journal 'Completed' } catch { Resolve-InstallationFailure $Operation $_ } } # Source: src/private/installation/Invoke-RepositoryInstallation.ps1 function Invoke-RepositoryInstallation { param([object]$Operation) $plan = $Operation.Plan $source = [pscustomobject]@{ Name = $plan.RepositoryName; Uri = $plan.RepositoryUri; ApiVersion = $plan.RepositoryApiVersion; Trusted = $plan.RepositoryTrusted } $lease = Open-RepositoryTransport $source $Operation.RepositoryName = $lease.Name $failure = $null try { Invoke-ScopedInstallation $Operation } catch { $failure = $_; throw } finally { Close-RepositoryTransport $lease $failure } } # Source: src/private/installation/Invoke-ScopedInstallation.ps1 function Invoke-ScopedInstallation { param([object]$Operation) Initialize-ControlledDirectory $Operation.DataRoot Initialize-ControlledDirectory $Operation.WorkRoot $lock = Enter-InstallationLock $Operation.LockPath try { Invoke-LockedInstallation $Operation } finally { $lock.Dispose() } } # Source: src/private/installation/Invoke-UpdateInstallation.ps1 function Invoke-UpdateInstallation { param([object]$Operation) Assert-InstallationConsent $Operation Write-InstallationPhase $Operation.Plan 'Checking' try { Invoke-RepositoryInstallation $Operation } catch [UnauthorizedAccessException] { Write-PMUError 'PMU.PermissionDenied' 'The selected scope is not writable. PMU does not elevate or change scope.' } finally { Write-InstallationPhase $Operation.Plan 'Completed' } } # Source: src/private/installation/Resolve-InstallationFailure.ps1 function Resolve-InstallationFailure { param([object]$Operation, [System.Management.Automation.ErrorRecord]$Failure) try { Restore-InstallationBackup $Operation.Journal $Operation } catch { $Operation.Journal.RecoveryStatus = 'Failed' Write-RecoveryJournalState $Operation.Journal 'RecoveryRequired' Write-PMUError 'PMU.RollbackFailed' 'Installation and rollback both failed. Recoverable evidence was preserved.' 'Do not delete the PMU journal or backup; inspect the selected installation before obtaining a fresh plan.' } if ($Failure.FullyQualifiedErrorId -like 'PMU.*') { throw $Failure } Write-ProviderError $Failure 'PMU.InstallationFailed' } # Source: src/private/installation/Test-InstalledPlanState.ps1 function Test-InstalledPlanState { param([object]$Operation) $plan = $Operation.Plan $current = @(Get-InstalledModuleIdentityList $plan.ModuleName $plan.Scope) Assert-SelectedInstallation $current $plan.ModuleGuid $target = @($current | Where-Object FullVersion -CEQ $plan.TargetVersion) if ($target.Count -eq 1) { return $true } $original = @($plan.ObservedInstalledVersions | Where-Object Scope -EQ $plan.Scope) if ((ConvertTo-Json @($current) -Depth 5 -Compress) -cne (ConvertTo-Json @($original) -Depth 5 -Compress)) { Write-PMUError 'PMU.InstalledStateChanged' 'The selected-scope installation changed after planning.' } Assert-FileTreeEqual $Operation.Record.InstalledTree @(Get-FileTreeInventory (Split-Path -Parent $plan.InstalledManifestPath)) 'PMU.InstalledStateChanged' $false } # Source: src/private/installation/Test-RestartRequired.ps1 function Test-RestartRequired { param([object]$Plan, [string]$Outcome) $loaded = @(Get-Module -Name $Plan.ModuleName | Where-Object Guid -EQ $Plan.ModuleGuid) if (-not $loaded.Count) { return $false } $target = [IO.Path]::GetFullPath($Plan.TargetInstalledPath) $replacedLoaded = @($loaded | Where-Object { [IO.Path]::GetFullPath($_.ModuleBase) -eq $target }).Count -gt 0 if ($Outcome -eq 'Installed' -and $replacedLoaded) { return $true } $behindTarget = @($loaded | Where-Object { (Compare-CompleteModuleVersion $Plan.TargetVersion (Get-ActiveModuleVersion $_).FullVersion) -gt 0 }) $behindTarget.Count -gt 0 } # Source: src/private/installation/Write-InstallationPhase.ps1 function Write-InstallationPhase { param([object]$Plan, [string]$Phase) if ([Console]::IsOutputRedirected) { return } if ([Environment]::GetCommandLineArgs() -contains '-NonInteractive') { return } $progressId = 17330 # Stable native progress activity for the single-module workflow. $activity = "Updating $($Plan.ModuleName)" if ($Phase -eq 'Completed') { Write-Progress -Id $progressId -Activity $activity -Completed return } Write-Progress -Id $progressId -Activity $activity -Status $Phase } # Source: src/private/locking/Assert-UnixDirectoryOwner.ps1 function Assert-UnixDirectoryOwner { param([string]$Path) $item = Get-Item -LiteralPath $Path -Force -ErrorAction Stop $owner = $item.PSObject.Properties['User'] if (-not $owner) { Write-PMUError 'PMU.PermissionDenied' 'The data-directory owner cannot be verified on this platform.' } if ($owner.Value -cnotin @([Environment]::UserName, 'root')) { Write-PMUError 'PMU.PermissionDenied' 'The PMU data directory is owned by an unrelated account.' } } # Source: src/private/locking/Assert-WindowsAccessRule.ps1 function Assert-WindowsAccessRule { param([object]$Rule, [string[]]$Trusted) if (-not $Rule.Allowed) { return } if (-not $Rule.CanWrite) { return } if ($Rule.Identity -notin $Trusted) { Write-PMUError 'PMU.PermissionDenied' 'The PMU data directory grants write access to an unrelated account or group.' } } # Source: src/private/locking/Assert-WindowsDirectoryDescriptor.ps1 function Assert-WindowsDirectoryDescriptor { param([object]$Descriptor) if ($Descriptor.Owner -notin $Descriptor.Trusted) { Write-PMUError 'PMU.PermissionDenied' 'The PMU data-directory owner is not trusted.' } foreach ($rule in $Descriptor.Access) { Assert-WindowsAccessRule $rule $Descriptor.Trusted } } # Source: src/private/locking/ConvertTo-WindowsAccessRule.ps1 function ConvertTo-WindowsAccessRule { param([object]$Rule) $write = [Security.AccessControl.FileSystemRights]::Write -bor [Security.AccessControl.FileSystemRights]::Delete -bor [Security.AccessControl.FileSystemRights]::ChangePermissions -bor [Security.AccessControl.FileSystemRights]::TakeOwnership [pscustomobject]@{ Identity = $Rule.IdentityReference.Value Allowed = $Rule.AccessControlType -eq [Security.AccessControl.AccessControlType]::Allow CanWrite = [bool]($Rule.FileSystemRights -band $write) } } # Source: src/private/locking/Enter-InstallationLock.ps1 function Enter-InstallationLock { param([string]$Path, [int]$TimeoutMilliseconds = 5000) $null = Assert-SafePath $Path $timer = [Diagnostics.Stopwatch]::StartNew() do { try { return [IO.File]::Open($Path, [IO.FileMode]::OpenOrCreate, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None) } catch [UnauthorizedAccessException] { Write-PMUError 'PMU.PermissionDenied' 'Cannot open the scope-specific PMU lock.' } catch [IO.IOException] { Start-Sleep -Milliseconds 50 } } while ($timer.ElapsedMilliseconds -lt $TimeoutMilliseconds) Write-PMUError 'PMU.LockTimeout' 'Another process holds the module/scope/root lock. Retry after it completes.' } # Source: src/private/locking/Enter-RepositoryRegistryLock.ps1 function Enter-RepositoryRegistryLock { param([string]$Root, [int]$TimeoutMilliseconds = 5000) Initialize-ControlledDirectory $Root Enter-InstallationLock (Join-Path $Root 'registry.lock') $TimeoutMilliseconds } # Source: src/private/locking/Get-InstallationLockIdentity.ps1 function Get-InstallationLockIdentity { param([object]$Plan) $identity = "$($Plan.ModuleName.ToLowerInvariant())|$($Plan.Scope)|$($Plan.DestinationRoot)" if ($IsWindows -or $IsMacOS) { $identity = $identity.ToLowerInvariant() } $hash = [Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($identity)) [Convert]::ToHexString($hash).ToLowerInvariant() } # Source: src/private/locking/Get-RepositoryProcessStart.ps1 function Get-RepositoryProcessStart { param([int]$ProcessId) try { $process = [Diagnostics.Process]::GetProcessById($ProcessId) } catch [ArgumentException] { return $null } try { "$($process.StartTime.ToUniversalTime().Ticks)" } catch { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Cannot verify the repository receipt owner process.' } finally { $process.Dispose() } } # Source: src/private/locking/Get-WindowsDirectoryDescriptor.ps1 function Get-WindowsDirectoryDescriptor { param([string]$Path) $identity = [Security.Principal.WindowsIdentity]::GetCurrent() try { $trusted = @($identity.User.Value, 'S-1-5-18', 'S-1-5-32-544', 'S-1-3-0') } finally { $identity.Dispose() } $acl = Get-Acl -LiteralPath $Path -ErrorAction Stop $rules = $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]) [pscustomobject]@{ Owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value Trusted = $trusted Access = @($rules | ForEach-Object { ConvertTo-WindowsAccessRule $_ }) } } # Source: src/private/locking/Initialize-ControlledDirectory.ps1 function Initialize-ControlledDirectory { param([string]$Path) $safe = Assert-SafePath $Path if ($IsWindows) { Initialize-WindowsControlledDirectory $safe; return } $mode = [IO.UnixFileMode]::UserRead -bor [IO.UnixFileMode]::UserWrite -bor [IO.UnixFileMode]::UserExecute $null = [IO.Directory]::CreateDirectory($safe, $mode) Assert-UnixDirectoryOwner $safe $unsafe = [IO.UnixFileMode]::GroupWrite -bor [IO.UnixFileMode]::OtherWrite if ([IO.File]::GetUnixFileMode($safe) -band $unsafe) { Write-PMUError 'PMU.PermissionDenied' 'PMU data directories must not be writable by group or other users.' } } # Source: src/private/locking/Initialize-WindowsControlledDirectory.ps1 function Initialize-WindowsControlledDirectory { param([string]$Path) $null = [IO.Directory]::CreateDirectory($Path) Assert-WindowsDirectoryDescriptor (Get-WindowsDirectoryDescriptor $Path) } # Source: src/private/locking/Resolve-PMUDataRoot.ps1 function Resolve-PMUDataRoot { param([string]$Scope) if ($IsWindows) { $folder = 'LocalApplicationData' if ($Scope -eq 'AllUsers') { $folder = 'CommonApplicationData' } return Assert-SafePath (Join-Path ([Environment]::GetFolderPath($folder)) 'PSModuleUpdate') } if ($Scope -eq 'CurrentUser') { return Assert-SafePath (Join-Path $HOME '.local/state/PSModuleUpdate') } if ($IsMacOS) { return Assert-SafePath '/Library/Application Support/PSModuleUpdate' } Assert-SafePath '/var/lib/PSModuleUpdate' } # Source: src/private/locking/Resolve-RepositoryTransportRoot.ps1 function Resolve-RepositoryTransportRoot { $folder = [Environment]::GetFolderPath('LocalApplicationData') Assert-SafePath (Join-Path $folder 'PSModuleUpdate/RepositoryTransport') } # Source: src/private/planning/Assert-PolicyValue.ps1 function Assert-PolicyValue { param([string]$Name, [object]$Value, [type]$Type, [string[]]$Allowed = @()) if ($null -eq $Value) { return } if ($Value -isnot $Type) { Write-PMUError 'PMU.InvalidRequest' "Policy field $Name has the wrong type." } if ($Allowed.Count -and $Value -cnotin $Allowed) { Write-PMUError 'PMU.InvalidRequest' "Policy field $Name has an unsupported value." } } # Source: src/private/planning/Assert-UpdatePolicy.ps1 function Assert-UpdatePolicy { param([hashtable]$Policy) if ($null -eq $Policy.Channel) { Write-PMUError 'PMU.InvalidRequest' 'Channel cannot be null.' } if ($null -eq $Policy.Scope) { Write-PMUError 'PMU.InvalidRequest' 'Scope cannot be null.' } Assert-PolicyValue 'Channel' $Policy.Channel ([string]) @('Stable', 'IncludePrerelease') Assert-PolicyValue 'Scope' $Policy.Scope ([string]) @('CurrentUser', 'AllUsers') Assert-PolicyValue 'ExpectedRepositoryUri' $Policy.ExpectedRepositoryUri ([uri]) Assert-PolicyValue 'VersionRange' $Policy.VersionRange ([string]) } # Source: src/private/planning/Complete-PlanningContext.ps1 function Complete-PlanningContext { param([object]$Context, [string]$RepositoryName, [pscredential]$Credential) $state = Get-PlanningInstallationState $Context.Module.Name $Context.Policy.Scope $Context.Reference.Guid Assert-ReferenceInstallationLayout $Context.Reference $state.Observed $state.RecoveryJournal $request = @{ Name = $Context.Module.Name; Repository = $RepositoryName; Prerelease = $Context.Policy.Channel -eq 'IncludePrerelease'; Version = $Context.Policy.VersionRange } $releases = @(Find-ProviderResource $request $Credential | ForEach-Object { ConvertTo-ProviderRelease $_ }) $target = Select-UpdateTarget $releases $Context.Policy.Channel $Context | Add-Member -NotePropertyMembers @{ Observed = $state.Observed; Installed = $state.Installed; InstalledTree = $state.InstalledTree; Target = $target } $Context } # Source: src/private/planning/ConvertTo-UpdatePlan.ps1 function ConvertTo-UpdatePlan { param([object]$Context) $fields = [ordered]@{ PSTypeName = 'PSModuleUpdate.UpdatePlan' } $groups = @((Get-PlanRuntimeField $Context), (Get-PlanInstallationField $Context), (Get-PlanTargetField $Context), (Get-PlanDecisionField $Context)) foreach ($group in $groups) { foreach ($entry in $group.GetEnumerator()) { $fields[$entry.Key] = $entry.Value } } [pscustomobject]$fields } # Source: src/private/planning/ConvertTo-UpdatePolicy.ps1 function ConvertTo-UpdatePolicy { param([hashtable]$Policy = @{}) $resolved = @{ Channel = 'Stable'; Scope = 'CurrentUser'; ExpectedRepositoryUri = $null; VersionRange = $null } foreach ($key in $Policy.Keys) { if (-not $resolved.ContainsKey($key)) { Write-PMUError 'PMU.InvalidRequest' 'Unknown policy field.' } $resolved[$key] = $Policy[$key] } Assert-UpdatePolicy $resolved $resolved } # Source: src/private/planning/Get-ActivePlanField.ps1 function Get-ActivePlanField { param([object]$Context) if (-not $Context.Active) { return @{ Version = $null; Path = $null; Scope = $null } } $scope = $Context.Observed | Where-Object ManifestPath -CEQ $Context.Active.ManifestPath | Select-Object -First 1 -ExpandProperty Scope @{ Version = $Context.Active.Version.FullVersion; Path = $Context.Active.ManifestPath; Scope = $scope } } # Source: src/private/planning/Get-EligibleRelease.ps1 function Get-EligibleRelease { param([object[]]$Releases, [string]$Channel) foreach ($release in $Releases) { if ($Channel -eq 'Stable' -and $release.Version.IsPrerelease) { continue } $release } } # Source: src/private/planning/Get-LivePlanRegistry.ps1 function Get-LivePlanRegistry { param() if (-not (Get-Variable -Name PmuLivePlans -Scope Script -ErrorAction SilentlyContinue)) { $script:PmuLivePlans = [System.Runtime.CompilerServices.ConditionalWeakTable[object, object]]::new() } ,$script:PmuLivePlans } # Source: src/private/planning/Get-PlanDecisionField.ps1 function Get-PlanDecisionField { param([object]$Context) $decision = Get-UpdateDecision $Context [ordered]@{ UpdateAvailable = $decision.Available Disposition = $decision.Disposition Reason = $decision.Reason RestartRequired = $decision.Restart RequiresRepositoryTrust = -not $Context.Source.Trusted RequiresLicenseAcceptance = $Context.Target.RequiresLicenseAcceptance DependencySummary = @($Context.Target.Dependencies) } } # Source: src/private/planning/Get-PlanInstallationField.ps1 function Get-PlanInstallationField { param([object]$Context) [ordered]@{ Scope = $Context.Policy.Scope DestinationRoot = $Context.Installed.Root InstalledVersion = $Context.Installed.FullVersion InstalledManifestPath = $Context.Installed.ManifestPath ObservedInstalledVersions = @($Context.Observed) RepositoryName = $Context.Source.Name RepositoryUri = $Context.Source.Uri RepositoryApiVersion = $Context.Source.ApiVersion RepositoryTrusted = $Context.Source.Trusted } } # Source: src/private/planning/Get-PlanningContext.ps1 function Get-PlanningContext { param([System.Management.Automation.PSModuleInfo]$Module, [object]$Repository, [hashtable]$Policy, [pscredential]$Credential) $context = Initialize-PlanningContext $Module $Repository $Policy $lease = Open-RepositoryTransport $context.Source $failure = $null try { Complete-PlanningContext $context $lease.Name $Credential } catch { $failure = $_; throw } finally { Close-RepositoryTransport $lease $failure } } # Source: src/private/planning/Get-PlanningInstallationState.ps1 function Get-PlanningInstallationState { param([string]$Name, [string]$Scope, [guid]$Guid) $state = Get-RecoveryAwareInstallation $Name $Scope $Guid $otherScope = 'AllUsers' if ($Scope -eq 'AllUsers') { $otherScope = 'CurrentUser' } $observed = @($state.Selected; Get-InstalledModuleIdentityList $Name $otherScope) $installed = Select-NewestInstallation $state.Selected $treePath = Split-Path -Parent $installed.ManifestPath if ($state.FromBackup -and $installed.NumericVersion -eq $state.Journal.TargetNumericVersion) { $treePath = $state.Journal.BackupPath } $recovery = $null if ($state.FromBackup) { $recovery = $state.Journal } [pscustomobject]@{ Observed = $observed; Installed = $installed; InstalledTree = @(Get-FileTreeInventory $treePath); RecoveryJournal = $recovery } } # Source: src/private/planning/Get-PlanRuntimeField.ps1 function Get-PlanRuntimeField { param([object]$Context) $active = Get-ActivePlanField $Context [ordered]@{ SchemaVersion = 1 CreatedAtUtc = [datetime]::UtcNow PmuVersion = '0.1.0-preview' ProviderName = $Context.Provider.Name ProviderVersion = "$($Context.Provider.Version)" ModuleName = $Context.Reference.Name ModuleGuid = $Context.Reference.Guid ActiveVersion = $active.Version ActivePath = $active.Path ActiveScope = $active.Scope } } # Source: src/private/planning/Get-PlanTargetField.ps1 function Get-PlanTargetField { param([object]$Context) $target = $Context.Target.Version [ordered]@{ Channel = $Context.Policy.Channel VersionRange = $Context.Policy.VersionRange TargetVersion = $target.FullVersion TargetNumericVersion = $target.NumericVersion TargetPrerelease = $target.Prerelease TargetIsPrerelease = $target.IsPrerelease TargetInstalledPath = Join-Path $Context.Installed.Root "$($Context.Reference.Name)/$($target.NumericVersion)" } } # Source: src/private/planning/Get-RecoveryAwareInstallation.ps1 function Get-RecoveryAwareInstallation { param([string]$Name, [string]$Scope, [guid]$Guid) $root = Resolve-ModuleInstallationRoot $Scope $identity = [pscustomobject]@{ ModuleName = $Name; ModuleGuid = $Guid; Scope = $Scope; DestinationRoot = $root; TargetNumericVersion = '0.0.0' } $operation = ConvertTo-InstallationOperation $identity $null @{} $null $journal = Read-RecoveryJournal $operation $selected = @(Get-RecoveryInstallationObservation $journal) $fromBackup = $selected.Count -gt 0 if ($fromBackup) { $selected += @(Get-InstalledModuleIdentityList $Name $Scope $journal.TargetPath) } else { $selected = @(Get-InstalledModuleIdentityList $Name $Scope) } Assert-SelectedInstallation $selected $Guid [pscustomobject]@{ Selected = $selected; Journal = $journal; FromBackup = $fromBackup } } # Source: src/private/planning/Get-UpdateDecision.ps1 function Get-UpdateDecision { param([object]$Context) $target = $Context.Target.Version.FullVersion $advancesActive = Test-TargetAdvancesActiveVersion $target $Context.Active $installedComparison = Compare-CompleteModuleVersion $target $Context.Installed.FullVersion $unchanged = @{ Available = $false; Disposition = 'UpToDate'; Reason = 'No release newer than both actual active and installed versions is available.'; Restart = $false } if (-not $advancesActive) { return $unchanged } if ($installedComparison -gt 0) { return @{ Available = $true; Disposition = 'UpdateAvailable'; Reason = 'An exact newer release is available.'; Restart = $false } } if ($Context.Active) { return @{ Available = $false; Disposition = 'AlreadyInstalledRestartRequired'; Reason = 'A newer version is already installed in the selected scope.'; Restart = $true } } $unchanged } # Source: src/private/planning/Initialize-PlanningContext.ps1 function Initialize-PlanningContext { param([System.Management.Automation.PSModuleInfo]$Module, [object]$Repository, [hashtable]$Policy) $resolved = ConvertTo-UpdatePolicy $Policy [pscustomobject]@{ Module = $Module; Policy = $resolved; Provider = Get-ProviderContext Reference = Get-ModuleReferenceIdentity $Module Active = Get-ActiveModuleIdentity $Module Source = Get-SelectedRepository $Repository $resolved.ExpectedRepositoryUri } } # Source: src/private/planning/Register-UpdatePlan.ps1 function Register-UpdatePlan { param([object]$Plan, [object]$Context, [bool]$RequiresCredential) $record = [pscustomobject]@{ Snapshot = ConvertTo-Json $Plan -Depth 12 -Compress Module = $Context.Module ReferencePath = $Context.Reference.ManifestPath WasLoaded = @(Get-Module -Name $Context.Module.Name | Where-Object { [object]::ReferenceEquals($_, $Context.Module) }).Count -gt 0 RequiresCredential = $RequiresCredential SourceKind = if ($Plan.RepositoryName) { 'Registered' } else { 'TemporaryUri' } InstalledTree = @($Context.InstalledTree) } (Get-LivePlanRegistry).Add($Plan, $record) $Plan } # Source: src/private/planning/Select-NewestInstallation.ps1 function Select-NewestInstallation { param([object[]]$Installations) $newest = $null foreach ($installation in $Installations) { if (-not $newest) { $newest = $installation; continue } if ((Compare-CompleteModuleVersion $installation.FullVersion $newest.FullVersion) -gt 0) { $newest = $installation } } $newest } # Source: src/private/planning/Select-UpdateTarget.ps1 function Select-UpdateTarget { param([object[]]$Releases, [string]$Channel) $target = $null foreach ($release in (Get-EligibleRelease $Releases $Channel)) { if (-not $target) { $target = $release; continue } if ((Compare-CompleteModuleVersion $release.Version.FullVersion $target.Version.FullVersion) -gt 0) { $target = $release } } if (-not $target) { Write-PMUError 'PMU.PackageNotFound' 'No eligible package was found in the explicit repository.' } $target } # Source: src/private/planning/Test-TargetAdvancesActiveVersion.ps1 function Test-TargetAdvancesActiveVersion { param([string]$TargetVersion, [object]$Active) if (-not $Active) { return $true } (Compare-CompleteModuleVersion $TargetVersion $Active.Version.FullVersion) -gt 0 } # Source: src/private/planning/Test-UpdatePlan.ps1 function Test-UpdatePlan { param([object]$Plan) $record = $null if (-not (Get-LivePlanRegistry).TryGetValue($Plan, [ref]$record)) { Write-PMUError 'PMU.InvalidPlan' 'A live plan returned by Get-PSModuleUpdate in this module instance is required.' } if ($Plan.PSObject.TypeNames[0] -cne 'PSModuleUpdate.UpdatePlan' -or $record.Snapshot -cne (ConvertTo-Json $Plan -Depth 12 -Compress)) { Write-PMUError 'PMU.InvalidPlan' 'The plan was modified or is not a live schema-1 plan.' } $record } # Source: src/private/provider/ConvertTo-ProviderRelease.ps1 function ConvertTo-ProviderRelease { param([object]$Resource) $version = "$($Resource.Version)" if ($Resource.Prerelease) { $version += "-$($Resource.Prerelease)" } $identity = ConvertTo-CompleteModuleVersion $version $license = Get-ProviderLicenseRequirement $Resource [pscustomobject]@{ Name = $Resource.Name; Version = $identity; RequiresLicenseAcceptance = $license; Dependencies = @($Resource.Dependencies | ForEach-Object { "$($_.Name)" }) } } # Source: src/private/provider/Find-ProviderResource.ps1 function Find-ProviderResource { param([hashtable]$Request, [pscredential]$Credential) $parameters = @{ Name = $Request.Name; Repository = $Request.Repository; Prerelease = $Request.Prerelease; ErrorAction = 'Stop'; WarningAction = 'SilentlyContinue'; Verbose = $false; Debug = $false } $parameters.Version = '*' if ($Request.Version) { $parameters.Version = $Request.Version } if ($Credential) { $parameters.Credential = $Credential } try { Microsoft.PowerShell.PSResourceGet\Find-PSResource @parameters } catch { Write-ProviderError $_ 'PMU.DiscoveryFailed' } } # Source: src/private/provider/Get-ProviderContext.ps1 function Get-ProviderContext { param() $provider = Get-Module Microsoft.PowerShell.PSResourceGet if (-not $provider) { try { $provider = Import-Module Microsoft.PowerShell.PSResourceGet -RequiredVersion 1.2.0 -PassThru -ErrorAction Stop } catch { Write-PMUError 'PMU.ProviderUnavailable' 'PSResourceGet 1.2.0 must be available; PMU does not install it.' } } if ($provider.Version -ne [version]'1.2.0') { Write-PMUError 'PMU.ProviderIncompatible' 'Only the qualified PSResourceGet 1.2.0 baseline is enabled.' } $provider } # Source: src/private/provider/Get-ProviderLicenseRequirement.ps1 function Get-ProviderLicenseRequirement { param([object]$Resource) $metadata = $Resource.AdditionalMetadata if (-not $metadata) { return $true } $keys = @($metadata.Keys | Where-Object { $_ -ieq 'RequireLicenseAcceptance' }) if ($keys.Count -ne 1) { return $true } $value = $metadata[$keys[0]] if ($value -is [bool]) { return $value } if ($value -isnot [string]) { return $true } $value -ine 'false' } # Source: src/private/provider/Get-ProviderMutationParameter.ps1 function Get-ProviderMutationParameter { param([object]$Operation) $plan = $Operation.Plan $parameters = @{ Name = $plan.ModuleName; Version = "[$($plan.TargetVersion)]"; Repository = Get-OperationRepositoryName $Operation Prerelease = $plan.TargetIsPrerelease; TrustRepository = [bool]$Operation.Consent.TrustRepository AcceptLicense = [bool]$Operation.Consent.AcceptLicense; Quiet = $true; Confirm = $false ErrorAction = 'Stop'; WarningAction = 'SilentlyContinue'; Verbose = $false; Debug = $false } if ($Operation.Credential) { $parameters.Credential = $Operation.Credential } $parameters } # Source: src/private/provider/Install-ProviderResource.ps1 function Install-ProviderResource { param([object]$Operation) $parameters = Get-ProviderMutationParameter $Operation $parameters.Scope = $Operation.Plan.Scope if ($Operation.Reinstall) { $parameters.Reinstall = $true } try { $null = Microsoft.PowerShell.PSResourceGet\Install-PSResource @parameters } catch { Write-ProviderError $_ 'PMU.InstallationFailed' } } # Source: src/private/provider/Save-ProviderResource.ps1 function Save-ProviderResource { param([object]$Operation) $parameters = Get-ProviderMutationParameter $Operation $parameters.Path = $Operation.StageRoot $parameters.IncludeXml = $true try { $null = Microsoft.PowerShell.PSResourceGet\Save-PSResource @parameters } catch { Write-ProviderError $_ 'PMU.InstallationFailed' } } # Source: src/private/provider/Write-ProviderError.ps1 function Write-ProviderError { param([System.Management.Automation.ErrorRecord]$Failure, [string]$DefaultId) $id = $DefaultId if ($Failure.Exception -is [UnauthorizedAccessException] -or $Failure.Exception.Message -match '(?i)access.+denied|permission denied') { $id = 'PMU.PermissionDenied' } if ($Failure.Exception.Message -match '(?i)\b401\b|\b403\b|unauthorized|authentication') { $id = 'PMU.AuthenticationFailed' } Write-PMUError $id 'The provider operation failed. Provider diagnostics were withheld to protect credentials.' 'Check repository authentication and scope permissions, then obtain a fresh plan. PMU never elevates or changes scope.' } # Source: src/private/recovery/Assert-BackupReferenceIdentity.ps1 function Assert-BackupReferenceIdentity { param([object]$Reference, [object]$Journal) if (-not $Journal) { Write-PMUError 'PMU.UnsupportedInstallation' 'The reference must identify an installed module or a validated unfinished-operation backup.' } $backup = Get-VerifiedRecoveryBackup $Journal 'PMU.RecoveryRequired' Assert-MatchingProperty $Reference $backup @('Name', 'Guid', 'ManifestPath') 'PMU.UnsupportedInstallation' if ($Reference.Version.FullVersion -cne $backup.Version.FullVersion) { Write-PMUError 'PMU.ModuleIdentityMismatch' 'The backup reference full version differs from the verified previous installation.' } } # Source: src/private/recovery/Assert-RecoveryJournal.ps1 function Assert-RecoveryJournal { param([object]$Journal, [object]$Operation) Assert-RecoveryJournalSchema $Journal $plan = $Operation.Plan $version = ConvertTo-CompleteModuleVersion $Journal.TargetVersion $expected = [pscustomobject]@{ SchemaVersion = 1; ModuleName = $plan.ModuleName; ModuleGuid = "$($plan.ModuleGuid)"; Scope = $plan.Scope; DestinationRoot = $plan.DestinationRoot; TargetNumericVersion = $version.NumericVersion } Assert-MatchingProperty $Journal $expected @('SchemaVersion', 'ModuleName', 'ModuleGuid', 'Scope', 'DestinationRoot', 'TargetNumericVersion') 'PMU.RecoveryRequired' Assert-RecoveryPath $Journal $Operation if ($Journal.State -notin @('Preparation', 'BackupReady', 'ReplacementInProgress', 'Verification', 'Completed', 'RecoveryRequired', 'RecoveryCompleted')) { Write-PMUError 'PMU.RecoveryRequired' 'Recovery journal state is invalid.' } } # Source: src/private/recovery/Assert-RecoveryJournalSchema.ps1 function Assert-RecoveryJournalSchema { param([object]$Journal) $fields = 'SchemaVersion ModuleName Scope DestinationRoot ModuleGuid PreviousVersion TargetVersion TargetNumericVersion TargetPath JournalPath StageRoot StagedPath BackupPath Reinstall State RecoveryStatus StagedTree BackupTree'.Split(' ') $actual = @($Journal.PSObject.Properties.Name | Sort-Object) if (($actual -join '|') -cne (($fields | Sort-Object) -join '|')) { Write-PMUError 'PMU.RecoveryRequired' 'The recovery journal schema is invalid.' } if ($Journal.Reinstall -isnot [bool]) { Write-PMUError 'PMU.RecoveryRequired' 'The recovery journal mutation flag is not Boolean.' } if ($Journal.StagedTree -isnot [array]) { Write-PMUError 'PMU.RecoveryRequired' 'The recovery reference tree is not an array.' } if ($Journal.BackupTree -isnot [array]) { Write-PMUError 'PMU.RecoveryRequired' 'The recovery backup tree is not an array.' } } # Source: src/private/recovery/Assert-RecoveryPath.ps1 function Assert-RecoveryPath { param([object]$Journal, [object]$Operation) $expected = @{ JournalPath = $Operation.JournalPath; StageRoot = $Operation.StageRoot; BackupPath = $Operation.BackupPath StagedPath = Join-Path $Operation.StageRoot "$($Journal.ModuleName)/$($Journal.TargetNumericVersion)" TargetPath = Join-Path $Operation.Plan.DestinationRoot "$($Journal.ModuleName)/$($Journal.TargetNumericVersion)" } foreach ($key in $expected.Keys) { if ($Journal.$key -cne $expected[$key]) { Write-PMUError 'PMU.RecoveryRequired' 'Recovery paths are not the expected PMU-controlled paths.' } $null = Assert-SafePath $Journal.$key } } # Source: src/private/recovery/Assert-RecoveryTargetIdentity.ps1 function Assert-RecoveryTargetIdentity { param([object]$Journal) $manifest = Join-Path $Journal.TargetPath "$($Journal.ModuleName).psd1" $data = Get-SurvivingManifestData $manifest $expected = @{ GUID = $Journal.ModuleGuid; ModuleVersion = $Journal.TargetNumericVersion } $errorId = @{ GUID = 'PMU.GuidMismatch'; ModuleVersion = 'PMU.ModuleIdentityMismatch' } foreach ($key in $expected.Keys) { if (-not $data[$key]) { continue } if ("$($data[$key])" -ine $expected[$key]) { Write-PMUError $errorId[$key] 'Recovery refuses to delete an installation with an unexpected manifest identity.' } } Assert-RecoveryTargetVersion $data $Journal } # Source: src/private/recovery/Assert-RecoveryTargetVersion.ps1 function Assert-RecoveryTargetVersion { param([hashtable]$Manifest, [object]$Journal) if (-not $Manifest['ModuleVersion']) { return } $version = Get-ManifestModuleVersion $Manifest if ($version.FullVersion -cnotin @($Journal.PreviousVersion, $Journal.TargetVersion)) { Write-PMUError 'PMU.ModuleIdentityMismatch' 'Recovery refuses to remove an unrelated full-version identity from the numeric target.' } } # Source: src/private/recovery/Clear-CompletedOperation.ps1 function Clear-CompletedOperation { param([object]$Operation) $journal = Read-RecoveryJournal $Operation if (-not $journal) { return } if ($journal.State -notin @('Completed', 'RecoveryCompleted')) { Write-PMUError 'PMU.RecoveryRequired' 'Unfinished recovery evidence cannot be discarded.' } foreach ($path in @($Operation.StageRoot, $Operation.BackupPath)) { $null = Assert-SafePath $path if (Test-Path -LiteralPath $path) { $null = @(Get-FileTreeInventory $path) Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction Stop } } } # Source: src/private/recovery/Clear-ControlledTarget.ps1 function Clear-ControlledTarget { param([object]$Journal) $expected = Join-Path $Journal.DestinationRoot "$($Journal.ModuleName)/$($Journal.TargetNumericVersion)" if ($expected -cne $Journal.TargetPath) { Write-PMUError 'PMU.RecoveryRequired' 'Refusing an unexpected removal path.' } $null = Assert-SafePath $expected if (Test-Path -LiteralPath $expected) { $null = @(Get-FileTreeInventory $expected) Assert-RecoveryTargetIdentity $Journal Remove-Item -LiteralPath $expected -Recurse -Force -ErrorAction Stop } } # Source: src/private/recovery/Complete-RecoveryTarget.ps1 function Complete-RecoveryTarget { param([object]$Journal, [object]$Operation) if (-not (Test-RecoveryTargetComplete $Journal)) { return $false } $Journal.RecoveryStatus = 'TargetVerified' Write-RecoveryJournalState $Journal 'Completed' $Operation.RecoveryStatus = 'TargetVerified' $true } # Source: src/private/recovery/ConvertTo-RecoveryJournal.ps1 function ConvertTo-RecoveryJournal { param([object]$Operation, [object[]]$StagedTree) $plan = $Operation.Plan [pscustomobject]@{ SchemaVersion = 1; ModuleName = $plan.ModuleName; Scope = $plan.Scope; DestinationRoot = $plan.DestinationRoot ModuleGuid = "$($plan.ModuleGuid)"; PreviousVersion = $plan.InstalledVersion; TargetVersion = $plan.TargetVersion TargetNumericVersion = $plan.TargetNumericVersion; TargetPath = $plan.TargetInstalledPath JournalPath = $Operation.JournalPath; StageRoot = $Operation.StageRoot; StagedPath = $Operation.StagedPath BackupPath = $Operation.BackupPath; Reinstall = $Operation.Reinstall State = 'Preparation'; RecoveryStatus = 'NotRequired'; StagedTree = @($StagedTree); BackupTree = @() } } # Source: src/private/recovery/Copy-InstallationBackup.ps1 function Copy-InstallationBackup { param([object]$Operation) $journal = $Operation.Journal $null = Assert-SafePath $Operation.Plan.TargetInstalledPath $journal.BackupTree = @(Get-FileTreeInventory $Operation.Plan.TargetInstalledPath) Copy-Item -LiteralPath $Operation.Plan.TargetInstalledPath -Destination $Operation.BackupPath -Recurse -ErrorAction Stop Assert-FileTreeEqual $journal.BackupTree @(Get-FileTreeInventory $Operation.BackupPath) Write-RecoveryJournalState $journal 'BackupReady' } # Source: src/private/recovery/Get-RecoveryInstallationObservation.ps1 function Get-RecoveryInstallationObservation { param([object]$Journal) if (-not $Journal) { return } if ($Journal.State -in @('Completed', 'RecoveryCompleted', 'Preparation')) { return } if (-not $Journal.Reinstall) { return } $backup = Get-VerifiedRecoveryBackup $Journal 'PMU.RecoveryRequired' [pscustomobject]@{ FullVersion = $backup.Version.FullVersion; NumericVersion = $backup.Version.NumericVersion; Prerelease = $backup.Version.Prerelease Scope = $Journal.Scope; Root = $Journal.DestinationRoot ManifestPath = Join-Path $Journal.TargetPath "$($Journal.ModuleName).psd1"; Guid = $backup.Guid } } # Source: src/private/recovery/Get-SurvivingManifestData.ps1 function Get-SurvivingManifestData { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return @{} } try { Import-PowerShellDataFile -LiteralPath $Path -ErrorAction Stop } catch { @{} } # A provider interruption can leave a truncated manifest in a journal-owned path. } # Source: src/private/recovery/Get-VerifiedRecoveryBackup.ps1 function Get-VerifiedRecoveryBackup { param([object]$Journal, [string]$ErrorId = 'PMU.RecoveryFailed') $manifest = Join-Path $Journal.BackupPath "$($Journal.ModuleName).psd1" $identity = Get-ManifestIdentity $manifest if ($identity.Guid -ne [guid]$Journal.ModuleGuid -or $identity.Version.FullVersion -cne $Journal.PreviousVersion) { Write-PMUError $ErrorId 'The surviving backup does not match the previous identity.' } Assert-FileTreeEqual @($Journal.BackupTree) @(Get-FileTreeInventory $Journal.BackupPath) $ErrorId $identity } # Source: src/private/recovery/Invoke-RecoveryRestoration.ps1 function Invoke-RecoveryRestoration { param([object]$Journal, [object]$Operation) try { if ($Journal.State -in @('Preparation', 'BackupReady')) { Write-RecoveryJournalState $Journal 'RecoveryCompleted' } else { Restore-InstallationBackup $Journal $Operation } } catch { Write-PMUError 'PMU.RecoveryFailed' 'Recovery could not be verified. The journal and surviving backup were preserved.' } } # Source: src/private/recovery/Read-RecoveryJournal.ps1 function Read-RecoveryJournal { param([object]$Operation) if (-not (Test-Path -LiteralPath $Operation.JournalPath)) { return $null } try { $null = Assert-SafePath $Operation.JournalPath $journal = Get-Content -LiteralPath $Operation.JournalPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop Assert-RecoveryJournal $journal $Operation return $journal } catch { Write-PMUError 'PMU.RecoveryRequired' 'The journal or its paths are unsafe. Preserve PMU data for manual inspection.' } } # Source: src/private/recovery/Repair-UnfinishedInstallation.ps1 function Repair-UnfinishedInstallation { param([object]$Operation) $journal = Read-RecoveryJournal $Operation if (-not $journal -or $journal.State -in @('Completed', 'RecoveryCompleted')) { return } Write-InstallationPhase $Operation.Plan 'Recovering' if (Complete-RecoveryTarget $journal $Operation) { return } Invoke-RecoveryRestoration $journal $Operation $null = (Get-LivePlanRegistry).Remove($Operation.Plan) Write-PMUError 'PMU.PlanStale' 'An unfinished operation was recovered. Obtain a fresh plan before another update.' } # Source: src/private/recovery/Restore-InstallationBackup.ps1 function Restore-InstallationBackup { param([object]$Journal, [object]$Operation) Assert-RecoveryJournal $Journal $Operation if ($Journal.Reinstall) { $null = Get-VerifiedRecoveryBackup $Journal } Clear-ControlledTarget $Journal if ($Journal.Reinstall) { Copy-Item -LiteralPath $Journal.BackupPath -Destination $Journal.TargetPath -Recurse -ErrorAction Stop Assert-FileTreeEqual @($Journal.BackupTree) @(Get-FileTreeInventory $Journal.TargetPath) 'PMU.RecoveryFailed' } $Journal.RecoveryStatus = 'Restored' Write-RecoveryJournalState $Journal 'RecoveryCompleted' } # Source: src/private/recovery/Test-RecoveryTargetComplete.ps1 function Test-RecoveryTargetComplete { param([object]$Journal) try { $identity = Get-ManifestIdentity (Join-Path $Journal.TargetPath "$($Journal.ModuleName).psd1") if ($identity.Guid -ne [guid]$Journal.ModuleGuid -or $identity.Version.FullVersion -cne $Journal.TargetVersion) { return $false } Assert-FileTreeEqual @($Journal.StagedTree) @(Get-FileTreeInventory $Journal.TargetPath) return $true } catch { return $false } } # Source: src/private/recovery/Write-DurableJournal.ps1 function Write-DurableJournal { param([object]$Journal) $path = Assert-SafePath $Journal.JournalPath $temporary = "$path.new" $null = Assert-SafePath $temporary $bytes = [Text.Encoding]::UTF8.GetBytes((ConvertTo-Json $Journal -Depth 12 -Compress)) $stream = [IO.File]::Open($temporary, [IO.FileMode]::Create, [IO.FileAccess]::Write, [IO.FileShare]::None) try { $stream.Write($bytes); $stream.Flush($true) } finally { $stream.Dispose() } [IO.File]::Move($temporary, $path, $true) } # Source: src/private/recovery/Write-RecoveryJournalState.ps1 function Write-RecoveryJournalState { param([object]$Journal, [string]$State) $transitions = @{ Preparation = @('Preparation', 'BackupReady', 'ReplacementInProgress', 'RecoveryCompleted') BackupReady = @('BackupReady', 'ReplacementInProgress', 'RecoveryCompleted') ReplacementInProgress = @('ReplacementInProgress', 'Verification', 'RecoveryRequired', 'RecoveryCompleted', 'Completed') Verification = @('Verification', 'Completed', 'RecoveryRequired', 'RecoveryCompleted') RecoveryRequired = @('RecoveryRequired', 'RecoveryCompleted', 'Completed') Completed = @('Completed'); RecoveryCompleted = @('RecoveryCompleted') } if ($State -notin $transitions[$Journal.State]) { Write-PMUError 'PMU.RecoveryRequired' 'Invalid recovery journal state transition.' } $Journal.State = $State Write-DurableJournal $Journal } # Source: src/private/repository/Assert-OwnedRepositoryConfiguration.ps1 function Assert-OwnedRepositoryConfiguration { param([object]$Registration, [object]$Receipt) $actual = [pscustomobject]@{ RegistrationName = $Registration.Name; UriHash = Get-RepositoryUriHash ([uri]$Registration.Uri) ApiVersion = "$($Registration.ApiVersion)"; Trusted = [bool]$Registration.Trusted; Priority = $Registration.Priority } Assert-MatchingProperty $actual $Receipt @('RegistrationName', 'UriHash', 'ApiVersion', 'Trusted', 'Priority') 'PMU.RepositoryRecoveryRequired' } # Source: src/private/repository/Assert-RepositoryBinding.ps1 function Assert-RepositoryBinding { param([object]$Plan, [string]$TransportName) $name = $Plan.RepositoryName if ($TransportName) { $name = $TransportName } if (-not $name) { $null = Assert-RepositoryUri ([uri]$Plan.RepositoryUri); return } $source = Get-RepositorySnapshot $name ([uri]$Plan.RepositoryUri) $expected = [pscustomobject]@{ Name = $name; Uri = $Plan.RepositoryUri ApiVersion = $Plan.RepositoryApiVersion; Trusted = $Plan.RepositoryTrusted } Assert-MatchingProperty $source $expected @('Name', 'Uri', 'ApiVersion', 'Trusted') 'PMU.RepositoryChanged' } # Source: src/private/repository/Assert-RepositoryName.ps1 function Assert-RepositoryName { param([string]$Name) if ([string]::IsNullOrWhiteSpace($Name) -or [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($Name)) { Write-PMUError 'PMU.InvalidRequest' 'Repository must be one literal registered name.' } } # Source: src/private/repository/Assert-RepositoryReceipt.ps1 function Assert-RepositoryReceipt { param([object]$Receipt) $expected = @('ApiVersion', 'OperationId', 'OwnerPid', 'OwnerStartedUtcTicks', 'Priority', 'RegistrationName', 'SchemaVersion', 'Trusted', 'UriHash') if ((@($Receipt.PSObject.Properties.Name | Sort-Object) -join ',') -cne ($expected -join ',')) { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Repository receipt schema is invalid.' } Assert-RepositoryReceiptIdentity $Receipt Assert-MatchingProperty $Receipt ([pscustomobject]@{ Trusted = $false; Priority = 50 }) @('Trusted', 'Priority') 'PMU.RepositoryRecoveryRequired' } # Source: src/private/repository/Assert-RepositoryReceiptIdentity.ps1 function Assert-RepositoryReceiptIdentity { param([object]$Receipt) $values = @( "$($Receipt.SchemaVersion)" -ceq '1' "$($Receipt.OperationId)" -cmatch '^[a-f0-9]{32}$' "$($Receipt.RegistrationName)" -ceq "pmu-uri-$($Receipt.OperationId)" "$($Receipt.OwnerPid)" -match '^[1-9][0-9]{0,9}$' "$($Receipt.OwnerStartedUtcTicks)" -match '^[0-9]{18}$' "$($Receipt.UriHash)" -cmatch '^[A-F0-9]{64}$' "$($Receipt.ApiVersion)" -cin @('Unknown', 'V2', 'V3', 'Local', 'NugetServer', 'ContainerRegistry') ) if ($false -in $values) { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Repository receipt identity is invalid.' } } # Source: src/private/repository/Assert-RepositoryUri.ps1 function Assert-RepositoryUri { param([uri]$Uri) if (-not $Uri.IsAbsoluteUri) { Write-PMUError 'PMU.InvalidRequest' 'Repository endpoints must be absolute.' } if ("$($Uri.UserInfo)$($Uri.Query)$($Uri.Fragment)") { Write-PMUError 'PMU.InvalidRequest' 'Repository endpoints must be absolute and must not contain credentials, queries, or fragments.' } $Uri.AbsoluteUri.TrimEnd('/') } # Source: src/private/repository/Clear-OwnedRepositoryReceipt.ps1 function Clear-OwnedRepositoryReceipt { param([object]$Entry) $registered = @(Get-RepositoryRegistration $Entry.Receipt.RegistrationName) if ($registered.Count -gt 1) { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Owned registration is ambiguous.' } if ($registered.Count) { Assert-OwnedRepositoryConfiguration $registered[0] $Entry.Receipt try { Microsoft.PowerShell.PSResourceGet\Unregister-PSResourceRepository -Name $Entry.Receipt.RegistrationName -Confirm:$false -ErrorAction Stop } catch { Write-ProviderError $_ 'PMU.RepositoryCleanupFailed' } } Remove-Item -LiteralPath (Assert-SafePath $Entry.Path) -ErrorAction Stop } # Source: src/private/repository/Close-OwnedRepositoryTransport.ps1 function Close-OwnedRepositoryTransport { param([object]$Lease) $root = Resolve-RepositoryTransportRoot $lock = Enter-RepositoryRegistryLock $root try { $receipt = Read-RepositoryReceipt $Lease.ReceiptPath $root Assert-MatchingProperty $receipt $Lease.Receipt @('OperationId', 'RegistrationName', 'OwnerPid', 'OwnerStartedUtcTicks', 'UriHash') 'PMU.RepositoryRecoveryRequired' if ($receipt.OwnerPid -ne $PID) { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'A different operation owns this registration.' } Clear-OwnedRepositoryReceipt ([pscustomobject]@{ Path = $Lease.ReceiptPath; Receipt = $receipt }) } finally { $lock.Dispose() } } # Source: src/private/repository/Close-RepositoryTransport.ps1 function Close-RepositoryTransport { param([object]$Lease, [System.Management.Automation.ErrorRecord]$Original) if (-not $Lease.ReceiptPath) { return } try { Close-OwnedRepositoryTransport $Lease } catch { Write-RepositoryCleanupFailure $Original } } # Source: src/private/repository/Complete-OwnedRepositoryBinding.ps1 function Complete-OwnedRepositoryBinding { param([object]$Lease, [object]$Registration) if ($Lease.Receipt.ApiVersion -ne 'Unknown') { Assert-OwnedRepositoryConfiguration $Registration $Lease.Receipt; return } $bound = $Lease.Receipt.PSObject.Copy() $bound.ApiVersion = "$($Registration.ApiVersion)" if ($bound.ApiVersion -eq 'Unknown') { Write-PMUError 'PMU.RepositoryUnavailable' 'The provider did not resolve its repository API.' } Assert-RepositoryReceipt $bound Assert-OwnedRepositoryConfiguration $Registration $bound $Lease.Receipt = $bound Write-BoundRepositoryReceipt $Lease $Lease.Source.ApiVersion = $bound.ApiVersion } # Source: src/private/repository/ConvertTo-RepositoryReceipt.ps1 function ConvertTo-RepositoryReceipt { param([object]$Source) $operationId = [guid]::NewGuid().ToString('N') [pscustomobject]@{ SchemaVersion = 1; OperationId = $operationId; RegistrationName = "pmu-uri-$operationId" OwnerPid = $PID; OwnerStartedUtcTicks = Get-RepositoryProcessStart $PID UriHash = Get-RepositoryUriHash ([uri]$Source.Uri) ApiVersion = "$($Source.ApiVersion)"; Trusted = $false; Priority = 50 } } # Source: src/private/repository/Get-OperationRepositoryName.ps1 function Get-OperationRepositoryName { param([object]$Operation) if ($Operation.RepositoryName) { return $Operation.RepositoryName } $Operation.Plan.RepositoryName } # Source: src/private/repository/Get-OwnedRepositoryName.ps1 function Get-OwnedRepositoryName { $root = Resolve-RepositoryTransportRoot foreach ($entry in Get-RepositoryReceiptEntry $root) { $registered = @(Get-RepositoryRegistration $entry.Receipt.RegistrationName) if (-not $registered.Count) { continue } Assert-OwnedRepositoryConfiguration $registered[0] $entry.Receipt $entry.Receipt.RegistrationName } } # Source: src/private/repository/Get-RepositoryReceiptEntry.ps1 function Get-RepositoryReceiptEntry { param([string]$Root) $null = Assert-SafePath $Root if (-not (Test-Path -LiteralPath $Root)) { return } Initialize-ControlledDirectory $Root foreach ($file in Get-ChildItem -LiteralPath $Root -Filter '*.json' -File) { [pscustomobject]@{ Path = $file.FullName; Receipt = Read-RepositoryReceipt $file.FullName $Root } } } # Source: src/private/repository/Get-RepositoryRegistration.ps1 function Get-RepositoryRegistration { param([string]$Name) try { @(Microsoft.PowerShell.PSResourceGet\Get-PSResourceRepository -ErrorAction Stop | Where-Object Name -CEQ $Name) } catch { Write-ProviderError $_ 'PMU.RepositoryUnavailable' } } # Source: src/private/repository/Get-RepositorySnapshot.ps1 function Get-RepositorySnapshot { param([string]$Name, [uri]$ExpectedUri) Assert-RepositoryName $Name try { $repositories = @(Microsoft.PowerShell.PSResourceGet\Get-PSResourceRepository -Name $Name -ErrorAction Stop) } catch { Write-ProviderError $_ 'PMU.RepositoryUnavailable' } if ($repositories.Count -ne 1) { Write-PMUError 'PMU.RepositoryUnavailable' 'Exactly one registered repository is required.' } $repository = $repositories[0] $uri = Assert-RepositoryUri $repository.Uri if ($ExpectedUri -and $uri -ne (Assert-RepositoryUri $ExpectedUri)) { Write-PMUError 'PMU.RepositoryChanged' 'Repository endpoint does not match policy.' } [pscustomobject]@{ Name = $repository.Name; Uri = $uri; ApiVersion = "$($repository.ApiVersion)"; Trusted = [bool]$repository.Trusted } } # Source: src/private/repository/Get-RepositorySnapshotByUri.ps1 function Get-RepositorySnapshotByUri { param([uri]$RepositoryUri, [uri]$ExpectedUri) $uri = Assert-RepositoryUri $RepositoryUri if ($ExpectedUri -and $uri -cne (Assert-RepositoryUri $ExpectedUri)) { Write-PMUError 'PMU.RepositoryChanged' 'Repository endpoint does not match policy.' } try { $repositories = @(Microsoft.PowerShell.PSResourceGet\Get-PSResourceRepository -ErrorAction Stop) } catch { Write-ProviderError $_ 'PMU.RepositoryUnavailable' } $owned = @(Get-OwnedRepositoryName) $selected = @($repositories | Where-Object { $_.Name -cnotin $owned -and ([uri]$_.Uri).AbsoluteUri.TrimEnd('/') -ceq $uri }) if ($selected.Count -gt 1) { Write-PMUError 'PMU.RepositoryUnavailable' 'RepositoryUri matches multiple consumer registrations.' } if (-not $selected.Count) { return [pscustomobject]@{ Name = $null; Uri = $uri; ApiVersion = 'Unknown'; Trusted = $false } } Get-RepositorySnapshot $selected[0].Name ([uri]$uri) } # Source: src/private/repository/Get-RepositoryUriHash.ps1 function Get-RepositoryUriHash { param([uri]$Uri) $normalized = Assert-RepositoryUri $Uri [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($normalized))) } # Source: src/private/repository/Get-SelectedRepository.ps1 function Get-SelectedRepository { param([object]$Selection, [uri]$ExpectedUri) if ($Selection -is [string]) { return Get-RepositorySnapshot $Selection $ExpectedUri } if ($Selection.Uri) { return Get-RepositorySnapshotByUri $Selection.Uri $ExpectedUri } Get-RepositorySnapshot $Selection.Name $ExpectedUri } # Source: src/private/repository/Invoke-OwnedRepositoryRegistration.ps1 function Invoke-OwnedRepositoryRegistration { param([object]$Source, [string]$Root) $receipt = ConvertTo-RepositoryReceipt $Source $path = Write-RepositoryReceipt $receipt $Root $lease = [pscustomobject]@{ Name = $receipt.RegistrationName; ReceiptPath = $path; Root = $Root; Receipt = $receipt; Source = $Source } try { Register-OwnedRepository $lease } catch { $failure = $_ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw $failure } try { Clear-OwnedRepositoryReceipt ([pscustomobject]@{ Path = $path; Receipt = $lease.Receipt }) } catch { Write-RepositoryCleanupFailure $failure } throw $failure } $lease } # Source: src/private/repository/Open-RepositoryTransport.ps1 function Open-RepositoryTransport { param([object]$Source, [int]$TimeoutMilliseconds = 5000) $null = Assert-RepositoryUri ([uri]$Source.Uri) if ($Source.Name) { return [pscustomobject]@{ Name = $Source.Name; ReceiptPath = $null } } $root = Resolve-RepositoryTransportRoot $lock = Enter-RepositoryRegistryLock $root $TimeoutMilliseconds try { Repair-RepositoryReceipt $root Invoke-OwnedRepositoryRegistration $Source $root } finally { $lock.Dispose() } } # Source: src/private/repository/Read-RepositoryReceipt.ps1 function Read-RepositoryReceipt { param([string]$Path, [string]$Root) $safe = Assert-SafePath $Path if ([IO.Path]::GetDirectoryName($safe) -cne (Assert-SafePath $Root)) { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Repository receipt path is invalid.' } if ((Get-Item -LiteralPath $safe).Length -gt 8192) { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Repository receipt is oversized.' } try { $receipt = Get-Content -LiteralPath $safe -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Repository receipt is unreadable or incomplete.' } Assert-RepositoryReceipt $receipt if ([IO.Path]::GetFileName($safe) -cne "$($receipt.OperationId).json") { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Repository receipt name does not match its operation.' } $receipt } # Source: src/private/repository/Register-OwnedRepository.ps1 function Register-OwnedRepository { param([object]$Lease) if (@(Get-RepositoryRegistration $Lease.Name).Count) { Remove-Item -LiteralPath $Lease.ReceiptPath -ErrorAction Stop Write-PMUError 'PMU.RepositoryUnavailable' 'The random transport name already exists; it will not be overwritten.' } $parameters = @{ Name = $Lease.Name; Uri = $Lease.Source.Uri; Priority = 50; ErrorAction = 'Stop' } if ($Lease.Receipt.ApiVersion -ne 'Unknown') { $parameters.ApiVersion = $Lease.Receipt.ApiVersion } try { $created = @(Microsoft.PowerShell.PSResourceGet\Register-PSResourceRepository @parameters -PassThru) } catch { Write-ProviderError $_ 'PMU.RepositoryUnavailable' } if ($created.Count -ne 1) { Write-PMUError 'PMU.RepositoryUnavailable' 'Transport registration did not return one provider identity.' } Complete-OwnedRepositoryBinding $Lease $created[0] $registered = @(Get-RepositoryRegistration $Lease.Name) if ($registered.Count -ne 1) { Write-PMUError 'PMU.RepositoryUnavailable' 'Transport registration could not be verified.' } Assert-OwnedRepositoryConfiguration $registered[0] $Lease.Receipt } # Source: src/private/repository/Repair-RepositoryReceipt.ps1 function Repair-RepositoryReceipt { param([string]$Root) foreach ($entry in Get-RepositoryReceiptEntry $Root) { if (Test-RepositoryReceiptOwnerAlive $entry.Receipt) { continue } Clear-OwnedRepositoryReceipt $entry } } # Source: src/private/repository/Test-RepositoryReceiptOwnerAlive.ps1 function Test-RepositoryReceiptOwnerAlive { param([object]$Receipt) if ([long]$Receipt.OwnerPid -gt [int]::MaxValue) { Write-PMUError 'PMU.RepositoryRecoveryRequired' 'Repository receipt process identity is invalid.' } $started = Get-RepositoryProcessStart ([int]$Receipt.OwnerPid) $started -ceq $Receipt.OwnerStartedUtcTicks } # Source: src/private/repository/Write-BoundRepositoryReceipt.ps1 function Write-BoundRepositoryReceipt { param([object]$Lease) $path = Assert-SafePath $Lease.ReceiptPath $temporary = Write-RepositoryReceipt $Lease.Receipt $Lease.Root '.binding' try { [IO.File]::Move($temporary, $path, $true) } finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -ErrorAction Stop } } } # Source: src/private/repository/Write-RepositoryCleanupFailure.ps1 function Write-RepositoryCleanupFailure { param([System.Management.Automation.ErrorRecord]$Original) if ($Original -and $Original.FullyQualifiedErrorId -match '^PMU\.') { $Original.Exception.Data['PMU.RepositoryCleanupFailed'] = $true throw $Original } Write-PMUError 'PMU.RepositoryCleanupFailed' 'Owned transport cleanup failed. Preserve its receipt and retry after correcting the provider or permissions.' } # Source: src/private/repository/Write-RepositoryReceipt.ps1 function Write-RepositoryReceipt { param([object]$Receipt, [string]$Root, [string]$Suffix = '') $path = Assert-SafePath (Join-Path $Root "$($Receipt.OperationId).json$Suffix") $bytes = [Text.Encoding]::UTF8.GetBytes((ConvertTo-Json $Receipt -Depth 4 -Compress)) $options = [IO.FileStreamOptions]::new() $options.Mode = [IO.FileMode]::CreateNew; $options.Access = [IO.FileAccess]::Write; $options.Share = [IO.FileShare]::None if (-not $IsWindows) { $options.UnixCreateMode = [IO.UnixFileMode]::UserRead -bor [IO.UnixFileMode]::UserWrite } $stream = [IO.FileStream]::new($path, $options) try { $stream.Write($bytes); $stream.Flush($true) } finally { $stream.Dispose() } $path } # Source: src/private/shared/Assert-MatchingProperty.ps1 function Assert-MatchingProperty { param([object]$Actual, [object]$Expected, [string[]]$Property, [string]$ErrorId) foreach ($name in $Property) { if ($Actual.$name -cne $Expected.$name) { Write-PMUError $ErrorId 'A required identity or environment precondition changed.' } } } # Source: src/private/shared/Assert-SafePath.ps1 function Assert-SafePath { param([string]$Path) $full = [IO.Path]::GetFullPath($Path) $cursor = $full while ($cursor) { if (Test-Path -LiteralPath $cursor) { $item = Get-Item -LiteralPath $cursor -Force -ErrorAction Stop if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { Write-PMUError 'PMU.UnsupportedInstallation' 'Symbolic links and reparse points are not supported in controlled paths.' } } $cursor = [IO.Path]::GetDirectoryName($cursor) } $full.TrimEnd([IO.Path]::DirectorySeparatorChar) } # Source: src/private/shared/Write-PMUError.ps1 function Write-PMUError { param([string]$Id, [string]$Message, [string]$Guidance = 'Obtain a fresh plan after correcting the reported condition.') $exception = [InvalidOperationException]::new($Message) $target = [pscustomobject]@{ RecoveryGuidance = $Guidance } throw [System.Management.Automation.ErrorRecord]::new($exception, $Id, 'InvalidOperation', $target) } # Source: src/private/staging/Clear-StagingRoot.ps1 function Clear-StagingRoot { param([object]$Operation) $expected = Join-Path $Operation.WorkRoot 'stage' if ($Operation.StageRoot -cne $expected) { Write-PMUError 'PMU.RecoveryRequired' 'Refusing to clear an unexpected staging path.' } $null = Assert-SafePath $expected if (-not (Test-Path -LiteralPath $expected)) { return } $null = @(Get-FileTreeInventory $expected) Remove-Item -LiteralPath $expected -Recurse -Force -ErrorAction Stop } # Source: src/private/staging/Initialize-StagedPackage.ps1 function Initialize-StagedPackage { param([object]$Operation) Write-InstallationPhase $Operation.Plan 'Staging' Clear-StagingRoot $Operation Initialize-ControlledDirectory $Operation.StageRoot Assert-RepositoryBinding $Operation.Plan $Operation.RepositoryName Save-ProviderResource $Operation $manifest = Join-Path $Operation.StagedPath "$($Operation.Plan.ModuleName).psd1" Assert-PackageIdentity (Get-ManifestIdentity $manifest) $Operation.Plan $tree = @(Get-FileTreeInventory $Operation.StagedPath) if ($tree.Count -lt 2) { Write-PMUError 'PMU.VerificationFailed' 'The extracted module tree is incomplete.' } Assert-PackageSource $Operation.StagedPath $Operation.Plan $tree } # Source: src/private/verification/Assert-FileTreeEqual.ps1 function Assert-FileTreeEqual { param([object[]]$Expected, [object[]]$Actual, [string]$ErrorId = 'PMU.VerificationFailed') $left = ConvertTo-Json @($Expected) -Depth 4 -Compress $right = ConvertTo-Json @($Actual) -Depth 4 -Compress if ($left -cne $right) { Write-PMUError $ErrorId 'The complete file tree does not match the verified reference.' } } # Source: src/private/verification/Assert-InstalledPackage.ps1 function Assert-InstalledPackage { param([object]$Operation, [object[]]$ReferenceTree) $plan = $Operation.Plan $path = Assert-SafePath $plan.TargetInstalledPath $expected = Join-Path (Resolve-ModuleInstallationRoot $plan.Scope) "$($plan.ModuleName)/$($plan.TargetNumericVersion)" if ($path -cne $expected) { Write-PMUError 'PMU.VerificationFailed' 'The provider destination does not match the planned scope root.' } $manifest = Join-Path $path "$($plan.ModuleName).psd1" Assert-PackageIdentity (Get-ManifestIdentity $manifest) $plan Assert-FileTreeEqual $ReferenceTree @(Get-FileTreeInventory $path) Assert-PackageSource $path $plan $discovered = @(Get-Module -ListAvailable $manifest | Where-Object Guid -EQ $plan.ModuleGuid) if ($discovered.Count -ne 1) { Write-PMUError 'PMU.VerificationFailed' 'The installed module is not discoverable at its exact manifest path.' } } # Source: src/private/verification/Assert-PackageIdentity.ps1 function Assert-PackageIdentity { param([object]$Identity, [object]$Plan) if ($Identity.Name -cne $Plan.ModuleName -or $Identity.Version.FullVersion -cne $Plan.TargetVersion) { Write-PMUError 'PMU.VerificationFailed' 'The package manifest does not match the exact planned name and complete version.' } if ($Identity.Guid -ne $Plan.ModuleGuid) { Write-PMUError 'PMU.GuidMismatch' 'The package manifest GUID differs from the planned identity.' } } # Source: src/private/verification/Assert-PackageSource.ps1 function Assert-PackageSource { param([string]$Path, [object]$Plan) $property = Get-PackageSourceProperty $Path if (-not $property) { return } if (-not $property.Value) { return } if ((Assert-RepositoryUri ([uri]$property.Value)) -cne $Plan.RepositoryUri) { Write-PMUError 'PMU.VerificationFailed' 'Package source metadata does not match the planned repository.' } } # Source: src/private/verification/Get-FileTreeInventory.ps1 function Get-FileTreeInventory { param([string]$Path) $root = Assert-SafePath $Path foreach ($item in Get-ChildItem -LiteralPath $root -Recurse -Force -ErrorAction Stop | Sort-Object FullName -CaseSensitive) { $null = Assert-SafePath $item.FullName $relative = [IO.Path]::GetRelativePath($root, $item.FullName).Replace('\', '/') if ($item.PSIsContainer) { [pscustomobject]@{ Path = "$relative/"; Length = 0; SHA256 = '' } continue } # PSResourceGet intentionally generates this root-level installation metadata file. if ($relative -ceq 'PSGetModuleInfo.xml') { continue } [pscustomobject]@{ Path = $relative; Length = $item.Length; SHA256 = (Get-FileHash -LiteralPath $item.FullName -Algorithm SHA256).Hash } } } # Source: src/private/verification/Get-PackageSourceProperty.ps1 function Get-PackageSourceProperty { param([string]$Path) $metadataPath = Join-Path $Path 'PSGetModuleInfo.xml' if (-not (Test-Path -LiteralPath $metadataPath)) { return } try { $metadata = Import-Clixml -LiteralPath $metadataPath -ErrorAction Stop } catch { Write-PMUError 'PMU.VerificationFailed' 'Provider source metadata is unreadable.' } $metadata.PSObject.Properties['RepositorySourceLocation'] } # Source: src/private/version/Compare-CompleteModuleVersion.ps1 function Compare-CompleteModuleVersion { param([string]$Left, [string]$Right) $a = ConvertTo-CompleteModuleVersion $Left $b = ConvertTo-CompleteModuleVersion $Right $comparison = ([version]$a.NumericVersion).CompareTo([version]$b.NumericVersion) if ($comparison) { return [Math]::Sign($comparison) } Compare-PrereleaseLabel $a.Prerelease $b.Prerelease } # Source: src/private/version/Compare-PrereleaseIdentifier.ps1 function Compare-PrereleaseIdentifier { param([string]$Left, [string]$Right) $leftNumeric = $Left -match '^\d+$' $rightNumeric = $Right -match '^\d+$' if ($leftNumeric -and $rightNumeric) { return [Math]::Sign(([System.Numerics.BigInteger]::Parse($Left)).CompareTo([System.Numerics.BigInteger]::Parse($Right))) } if ($leftNumeric -ne $rightNumeric) { if ($leftNumeric) { return -1 } return 1 } [Math]::Sign([string]::CompareOrdinal($Left, $Right)) } # Source: src/private/version/Compare-PrereleaseLabel.ps1 function Compare-PrereleaseLabel { param([string]$Left, [string]$Right) if ($Left -ceq $Right) { return 0 } if (-not $Left) { return 1 } if (-not $Right) { return -1 } Compare-PrereleaseSegmentList $Left.Split('.') $Right.Split('.') } # Source: src/private/version/Compare-PrereleaseSegmentList.ps1 function Compare-PrereleaseSegmentList { param([string[]]$Left, [string[]]$Right) for ($index = 0; $index -lt [Math]::Min($Left.Count, $Right.Count); $index++) { $comparison = Compare-PrereleaseIdentifier $Left[$index] $Right[$index] if ($comparison) { return $comparison } } [Math]::Sign($Left.Count - $Right.Count) } # Source: src/private/version/ConvertTo-CompleteModuleVersion.ps1 function ConvertTo-CompleteModuleVersion { param([string]$Version) if ($Version -notmatch '^(?<numeric>\d+\.\d+(?:\.\d+){0,2})(?:-(?<label>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$') { Write-PMUError 'PMU.UnsupportedProviderBehavior' 'The module version has an unsupported form.' } $numeric = [version]$Matches.numeric $label = $Matches['label'] if (@($label -split '\.' | Where-Object { $_ -match '^0\d+$' }).Count) { Write-PMUError 'PMU.UnsupportedProviderBehavior' 'Numeric prerelease identifiers cannot have leading zeros.' } [pscustomobject]@{ FullVersion = $Version; NumericVersion = $numeric.ToString(); Prerelease = "$label"; IsPrerelease = [bool]$label } } |