Private/State/Set-StateToRegistry.ps1
|
function Set-StateToRegistry { <# .SYNOPSIS Saves deferral state to registry or file fallback .DESCRIPTION Persists deferral state for an application to the registry, falling back to file if registry not accessible .PARAMETER State The DeferralState object to save #> [CmdletBinding()] param( [Parameter(Mandatory)] [DeferralState]$State ) $config = Get-ModuleConfiguration $appKey = Join-Path $config.StateRegistryKey $State.AppId try { # Ensure parent key exists if (-not (Test-Path $config.StateRegistryKey)) { New-Item -Path $config.StateRegistryKey -Force -ErrorAction Stop | Out-Null } # Ensure app key exists if (-not (Test-Path $appKey)) { New-Item -Path $appKey -Force -ErrorAction Stop | Out-Null } # Save state values Set-ItemProperty -Path $appKey -Name 'DeferralCount' -Value $State.DeferralCount -ErrorAction Stop Set-ItemProperty -Path $appKey -Name 'Phase' -Value $State.Phase.ToString() -ErrorAction Stop Set-ItemProperty -Path $appKey -Name 'MaxDeferrals' -Value $State.MaxDeferrals -ErrorAction Stop if ($State.FirstNotification -ne [datetime]::MinValue) { Set-ItemProperty -Path $appKey -Name 'FirstNotification' -Value $State.FirstNotification.ToString('o') } if ($State.LastDeferral -ne [datetime]::MinValue) { Set-ItemProperty -Path $appKey -Name 'LastDeferral' -Value $State.LastDeferral.ToString('o') } if ($State.TargetVersion) { Set-ItemProperty -Path $appKey -Name 'TargetVersion' -Value $State.TargetVersion } if ($State.DeadlineDate -ne [datetime]::MinValue) { Set-ItemProperty -Path $appKey -Name 'DeadlineDate' -Value $State.DeadlineDate.ToString('o') } Write-PatchLog "Saved deferral state for $($State.AppId) to registry" -Type Info } catch { # Registry not accessible - use file fallback Write-PatchLog "Registry not accessible, using file fallback for $($State.AppId)" -Type Warning -NoEventLog Set-StateToFile -State $State } } function Remove-StateFromRegistry { <# .SYNOPSIS Removes deferral state from registry .DESCRIPTION Clears persisted deferral state for an application .PARAMETER AppId The application ID to clear state for #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$AppId ) $config = Get-ModuleConfiguration $appKey = Join-Path $config.StateRegistryKey $AppId try { if (Test-Path $appKey) { Remove-Item -Path $appKey -Recurse -Force Write-PatchLog "Cleared deferral state for $AppId" -Type Info } } catch { Write-PatchLog "Failed to clear state for $AppId : $_" -Type Warning } } function Initialize-DeferralState { <# .SYNOPSIS Initializes deferral state for a new update .DESCRIPTION Creates initial deferral state when an update is first detected .PARAMETER AppId The application ID .PARAMETER TargetVersion The version being updated to .PARAMETER Config Optional configuration for deferral settings #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$AppId, [Parameter()] [string]$TargetVersion, [Parameter()] [PsPatchMyPCConfig]$Config ) # Normalize TargetVersion (some winget packages report empty available versions) if ([string]::IsNullOrWhiteSpace($TargetVersion)) { $TargetVersion = 'Latest' } # Check for existing state $existingState = Get-StateFromRegistry -AppId $AppId # If state exists and version matches, return existing if ($existingState.TargetVersion -eq $TargetVersion) { return $existingState } # New version detected - create new state (reset deferrals per Nudge pattern) $state = [DeferralState]::new() $state.AppId = $AppId # Ensure TargetVersion is never empty if ([string]::IsNullOrWhiteSpace($TargetVersion)) { $TargetVersion = 'Latest' } $state.TargetVersion = $TargetVersion $state.FirstNotification = [datetime]::UtcNow # Get deferral config if (-not $Config) { $Config = Get-PatchMyPCConfig } $state.MaxDeferrals = $Config.Deferrals.MaxCount $state.DeadlineDate = [datetime]::UtcNow.AddDays($Config.Deferrals.DeadlineDays) $state.Phase = [DeferralPhase]::Initial # Save initial state (Set-StateToRegistry handles fallback internally) Set-StateToRegistry -State $state Write-PatchLog "Initialized deferral state for $AppId (target: $TargetVersion, deadline: $($state.DeadlineDate))" -Type Info return $state } function Set-StateToFile { <# .SYNOPSIS Saves deferral state to file (fallback when registry not accessible) #> [CmdletBinding()] param( [Parameter(Mandatory)] [DeferralState]$State ) try { $config = Get-ModuleConfiguration $candidateDirs = @() if ($config.StatePath) { $candidateDirs += $config.StatePath } if ($env:TEMP) { $candidateDirs += (Join-Path $env:TEMP 'PsPatchMyPC\State') } if ($env:TMP) { $candidateDirs += (Join-Path $env:TMP 'PsPatchMyPC\State') } $candidateDirs = $candidateDirs | Select-Object -Unique $stateDir = $null foreach ($dir in $candidateDirs) { try { if (-not (Test-Path $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } # Write test $probe = Join-Path $dir ("._probe_{0}.tmp" -f ([guid]::NewGuid().ToString())) Set-Content -Path $probe -Value 'probe' -Encoding Ascii -Force Remove-Item -Path $probe -Force -ErrorAction SilentlyContinue $stateDir = $dir break } catch { continue } } if (-not $stateDir) { throw "No writable state directory available (tried: $($candidateDirs -join ', '))" } $stateFile = Join-Path $stateDir "$($State.AppId -replace '[^a-zA-Z0-9]', '_').json" $stateData = @{ AppId = $State.AppId DeferralCount = $State.DeferralCount FirstNotification = $State.FirstNotification.ToString('o') LastDeferral = $State.LastDeferral.ToString('o') TargetVersion = $State.TargetVersion DeadlineDate = $State.DeadlineDate.ToString('o') Phase = $State.Phase.ToString() MaxDeferrals = $State.MaxDeferrals } $stateData | ConvertTo-Json | Out-File -FilePath $stateFile -Encoding UTF8 -Force } catch { Write-PatchLog "Failed to save state to file for $($State.AppId): $_" -Type Warning -NoEventLog } } |