Runtime/Invoke-OSDStageEnvironmentVariables.ps1
|
[CmdletBinding()] param([string]$ConfigPath = (Join-Path $PSScriptRoot 'config.json')) function Assert-OSDStageEnvironmentOperation { param([Parameter(Mandatory)]$Operation) if ([string]$Operation.Action -notin @('Set','Remove','AppendPath')) { throw 'Unsupported environment operation.' } $name = [string]$Operation.Name if ([string]::IsNullOrWhiteSpace($name) -or $name.Length -gt 255 -or $name -ne $name.Trim() -or $name -match '[=\x00-\x1F]') { throw 'Environment variable names must contain 1 to 255 characters, without equals signs, control characters or surrounding whitespace.' } if ($Operation.Action -eq 'Set') { if ($Operation.Value -isnot [string] -or $Operation.Value.Length -gt 32766 -or $Operation.Value.Contains([string][char]0)) { throw 'Environment values must be strings of at most 32766 characters without NUL characters.' } if ([string]$Operation.Kind -notin @('String','ExpandString')) { throw 'Unsupported environment value kind.' } } if ($Operation.Action -eq 'AppendPath') { if ($name -ine 'Path' -or @($Operation.Entries).Count -eq 0) { throw 'AppendPath requires at least one machine PATH entry.' } foreach ($entry in @($Operation.Entries)) { if ($entry -isnot [string] -or [string]::IsNullOrWhiteSpace($entry) -or $entry.Length -gt 32766 -or $entry -match '[;"\x00-\x1F]' -or $entry -ne $entry.Trim() -or $entry -notmatch '^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+|%[^%=\\/]+%(?:[\\/]|$))') { throw 'Each PATH entry must be an absolute drive/UNC path or a percent-variable reference, without quotes, semicolons, control characters or surrounding whitespace.' } } } } function Assert-OSDStageEnvironmentConfiguration { param([Parameter(Mandatory)]$Configuration) $id = [guid]::Empty if ([int]$Configuration.SchemaVersion -ne 2 -or -not [guid]::TryParse([string]$Configuration.DeploymentId, [ref]$id) -or [string]::IsNullOrWhiteSpace([string]$Configuration.ResultPath) -or @($Configuration.Operations).Count -eq 0) { throw 'Invalid environment configuration.' } foreach ($operation in @($Configuration.Operations)) { Assert-OSDStageEnvironmentOperation -Operation $operation } } function Get-OSDStageEnvironmentPathIdentity { param([Parameter(Mandatory)][AllowEmptyString()][string]$Entry) # Lexical comparison only: never expand against the preparation PC or resolve files. $identity = $Entry.Trim().Trim('"').Replace('/', '\') if ($identity -notmatch '^[A-Za-z]:\\$') { $identity = $identity.TrimEnd('\') } $identity.ToUpperInvariant() } function Invoke-OSDStageEnvironmentOperation { param( [Parameter(Mandatory)]$Operation, [Parameter(Mandatory)][Microsoft.Win32.RegistryKey]$EnvironmentKey ) Assert-OSDStageEnvironmentOperation -Operation $Operation $name = [string]$Operation.Name $action = [string]$Operation.Action $exists = $name -in @($EnvironmentKey.GetValueNames()) $outcome = 'Unchanged' $kind = $null $added = 0 if ($action -eq 'Remove') { if ($exists) { $EnvironmentKey.DeleteValue($name, $false); $outcome = 'Removed' } else { $outcome = 'AlreadyAbsent' } } else { $before = $null $oldKind = $null if ($exists) { $oldKind = $EnvironmentKey.GetValueKind($name) if ($oldKind -notin @([Microsoft.Win32.RegistryValueKind]::String, [Microsoft.Win32.RegistryValueKind]::ExpandString)) { throw 'Existing environment value has a non-string registry type.' } $before = [string]$EnvironmentKey.GetValue($name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } if ($action -eq 'Set') { $value = [string]$Operation.Value $kind = [Microsoft.Win32.RegistryValueKind]([string]$Operation.Kind) } else { $kind = if ($exists) { $oldKind } else { [Microsoft.Win32.RegistryValueKind]::ExpandString } $value = if ($exists) { $before } else { '' } $seen = @{} foreach ($part in @($value -split ';')) { $seen[(Get-OSDStageEnvironmentPathIdentity -Entry $part)] = $true } foreach ($entry in @($Operation.Entries)) { $identity = Get-OSDStageEnvironmentPathIdentity -Entry $entry if (-not $seen.ContainsKey($identity)) { if ($value.Length -gt 0 -and -not $value.EndsWith(';')) { $value += ';' } $value += $entry $seen[$identity] = $true $added++ } } } if ($value.Length -gt 32766) { throw 'Resulting environment value exceeds 32766 characters.' } if (-not $exists -or $before -cne $value -or $oldKind -ne $kind) { $EnvironmentKey.SetValue($name, $value, $kind) $outcome = if ($action -eq 'AppendPath') { 'Appended' } else { 'Set' } } } [pscustomobject]@{ Name=$name;Action=$action;Status='Succeeded';Outcome=$outcome Changed=($outcome -in @('Set','Removed','Appended'));Kind=[string]$kind AddedPathEntryCount=$added;Error=$null } } function Send-OSDStageEnvironmentChange { # Notify listeners once; existing process environments and services are not rewritten. if (-not ('OSDStage.EnvironmentNotification' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; namespace OSDStage { public static class EnvironmentNotification { [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr SendMessageTimeout(IntPtr window, uint message, UIntPtr wParam, string lParam, uint flags, uint timeout, out UIntPtr result); } } '@ -ErrorAction Stop } $result = [UIntPtr]::Zero $sent = [OSDStage.EnvironmentNotification]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 1000, [ref]$result) return ($sent -ne [IntPtr]::Zero) } function Invoke-OSDStageEnvironmentConfiguration { param( [Parameter(Mandatory)]$Configuration, [Parameter(Mandatory)][Microsoft.Win32.RegistryKey]$EnvironmentKey ) Assert-OSDStageEnvironmentConfiguration -Configuration $Configuration $items = [System.Collections.Generic.List[object]]::new() foreach ($operation in @($Configuration.Operations)) { try { $items.Add((Invoke-OSDStageEnvironmentOperation -Operation $operation -EnvironmentKey $EnvironmentKey)) } catch { # Do not echo configuration values or native exception messages into logs/results. $items.Add([pscustomobject]@{ Name=[string]$operation.Name;Action=[string]$operation.Action;Status='Failed' Outcome='Failed';Changed=$false;Kind=$null;AddedPathEntryCount=0 Error=('Environment operation failed ({0}).' -f $_.Exception.GetType().Name) }) } } $failed = @($items | Where-Object Status -eq 'Failed').Count $changed = @($items | Where-Object Changed).Count $notification = $null if ($changed -gt 0) { try { $notification = Send-OSDStageEnvironmentChange } catch { $notification = $false } } [pscustomobject]@{ SchemaVersion=2;DeploymentId=[string]$Configuration.DeploymentId Status=if ($failed) { 'Failed' } else { 'Succeeded' } Scope='Machine';OperationCount=$items.Count;SuccessfulOperationCount=($items.Count-$failed) FailedOperationCount=$failed;ChangedOperationCount=$changed NotificationSucceeded=$notification;RestartNeeded=$false;RebootRequested=$false ExistingProcessesRefreshed=$false;Items=@($items) Error=if ($failed) { "$failed environment operation(s) failed." } else { $null } } } # Dot-sourcing exposes shared validation to module staging without performing writes. if ($MyInvocation.InvocationName -eq '.') { return } $ErrorActionPreference = 'Stop' $environmentKey = $null $configuration = $null try { if (Test-Path -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT') { throw 'Environment runtime requires full Windows.' } $configuration = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop Assert-OSDStageEnvironmentConfiguration -Configuration $configuration $environmentKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('SYSTEM\CurrentControlSet\Control\Session Manager\Environment', $true) if ($null -eq $environmentKey) { throw 'Machine environment registry key is unavailable.' } $record = Invoke-OSDStageEnvironmentConfiguration -Configuration $configuration -EnvironmentKey $environmentKey $resultPath = [Environment]::ExpandEnvironmentVariables([string]$configuration.ResultPath) New-Item -ItemType Directory -Path (Split-Path $resultPath -Parent) -Force | Out-Null $record | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $resultPath -Encoding UTF8 if ($record.Status -eq 'Failed') { exit 1 } exit 0 } catch { # A malformed config cannot authorize any environment write. Write-Error 'Environment configuration failed; no values are included in this diagnostic.' -ErrorAction Continue exit 1 } finally { if ($null -ne $environmentKey) { $environmentKey.Dispose() } } |