Payload/Start-HDTResume.ps1
|
<#
.SYNOPSIS The RunOnce payload: reconcile the boot, then resume the task sequence. .DESCRIPTION The engine is launched at logon by a RunOnce entry re-registered each leg, pointing at C:\HDT\Start-HDTResume.ps1, which loads state.json and continues at the next step. THIS IS NOT A MODULE FILE. The loader dot-sources Private\ and Public\ only, so Payload\ ships as a script, is staged to C:\HDT\, and is launched by powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\HDT\Start-HDTResume.ps1 which is the command Set-HDTAutoLogon writes into RunOnce. THE RECONCILE RUNS FIRST, BEFORE ANYTHING ELSE: "if the state document says the run is finished, failed, or missing, it clears autologon, the LSA secret, the RunOnce entry, and C:\HDT\state.json before doing anything else." A machine that boots with a finished or abandoned run must be disarmed on THAT boot, not on the next one, and certainly not only when AutoLogonCount finally runs out. So nothing here touches the sequence until the reconcile has answered. When it answers Teardown this script exits 0 and the machine is left disarmed. When it answers Resume, the log context is rebuilt from the state's runId and seq - so the JSONL numbering continues across the reboot - the sequence is re-imported from the workspace, and the loop is called with the state that was reconciled. IT IS TESTED BY PARSING AND INSPECTING IT rather than by running it. Running it for real means a module under C:\HDT\Modules, real service adapters and a machine to reboot, which belongs to phase 04's integration layer. tests/unit/StartHDTResumePayload.Tests.ps1 asserts what can be proven from a desk: that it parses under both engines, that the reconcile precedes the loop, and that the reconciled state is what the loop is given. .PARAMETER ModulePath The staged Hephaestus module. C:\HDT\Modules\Hephaestus on a deployed machine; a test points it elsewhere. .PARAMETER StatePath The run state document. It lives at C:\HDT\state.json in the full OS. .PARAMETER WorkspaceRoot Where the sequence lives. The sequence is found at <WorkspaceRoot>\Sequences\<sequenceId>\sequence.yaml, with the id taken from the state document rather than guessed. .PARAMETER SequencePath The sequence file, when it is not where WorkspaceRoot would put it. .PARAMETER LogPath The log directory for this leg. Defaults to Get-HDTLogPath for the full OS phase. .PARAMETER MaxAgeHour How stale a Running state may be before the run counts as abandoned. Passed through to the reconcile. .EXAMPLE powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\HDT\Start-HDTResume.ps1 What RunOnce runs. .EXAMPLE .\Start-HDTResume.ps1 -ModulePath C:\src\Hephaestus -StatePath C:\HDT\state.json #> [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string] $ModulePath = 'C:\HDT\Modules\Hephaestus', [Parameter()] [ValidateNotNullOrEmpty()] [string] $StatePath = 'C:\HDT\state.json', [Parameter()] [ValidateNotNullOrEmpty()] [string] $WorkspaceRoot = 'C:\HDT', [Parameter()] [ValidateNotNullOrEmpty()] [string] $SequencePath, [Parameter()] [ValidateNotNullOrEmpty()] [string] $LogPath, [Parameter()] [ValidateRange(0, [int]::MaxValue)] [int] $MaxAgeHour = 12 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module -Name $ModulePath -Force -ErrorAction Stop # The real adapters. A fake never appears in this file: it runs on a machine # mid-deployment, and the whole point of the injection is that THIS is the one # place the real ones are built. $fileSystem = New-HDTFileSystem $clock = New-HDTClock $registry = New-HDTRegistryService $lsa = New-HDTLsaService # FullOS, and it is not a guess either: this payload runs from RunOnce on a # deployed Windows install, which has shutdown.exe and does not have wpeutil. $power = New-HDTPowerService -Environment FullOS $process = New-HDTProcessService $scriptInvoker = New-HDTScriptInvoker -Root $WorkspaceRoot $environment = New-HDTEnvironmentProvider $cim = New-HDTCimProvider $logRoot = $LogPath if ([string]::IsNullOrWhiteSpace($logRoot)) { $logRoot = Get-HDTLogPath -Phase FullOS } $fileSystem.CreateDirectory($logRoot) # The boot log's counter is seeded from the state document BEFORE the reconcile # runs, because DESIGN 4.4.2's monotonic seq has to survive the reboot - and the # reconcile's own reboot.resume record is written through this context, in the # middle of the stream. A context left at zero restarts the numbering at 1 there, # which is exactly the ambiguity the counter exists to prevent. # # READ-ONLY AND BEST-EFFORT, so it is not the reconcile happening early: nothing # is decided or acted on here. A missing or corrupt document is the case the # reconcile is about to disarm, and a restarted counter is the least of it. $bootSeq = [long] 0 try { if ($fileSystem.TestPath($StatePath)) { $bootSeq = [long] (ConvertFrom-Json -InputObject $fileSystem.ReadAllText($StatePath)).seq } } catch { $bootSeq = [long] 0 } # A bootstrap log context, so the reconcile's own decision is recorded even when # it turns out there is no run to resume. Its run id is replaced below by the one # from the state document when there is one. $bootLog = New-HDTLogContext -RunId 'boot' -Phase FullOS -LogPath $logRoot ` -FileSystem $fileSystem -Clock $clock -Seq $bootSeq # BEFORE ANYTHING ELSE (DESIGN 4.5.2). $decision = Invoke-HDTBootReconciliation -StatePath $StatePath -FileSystem $fileSystem ` -Registry $registry -Lsa $lsa -Clock $clock -LogContext $bootLog -MaxAgeHour $MaxAgeHour if ($decision.Action -eq 'Teardown') { Write-HDTLog -Context $bootLog -Event 'reboot.teardown' ` -Message ("Nothing to resume ({0}); the machine has been disarmed." -f $decision.Reason) exit 0 } $state = $decision.State $sequenceFile = $SequencePath if ([string]::IsNullOrWhiteSpace($sequenceFile)) { $sequenceFile = Get-HDTWorkspacePath -Root $WorkspaceRoot -Kind TaskSequences ` -ChildPath ([string] $state.sequenceId), 'sequence.yaml' } $sequence = Import-HDTSequenceDocument -Path $sequenceFile -FileSystem $fileSystem # The state's runId and the BOOT LOG'S seq are what make this leg continuous with # the last one. Not $state.seq: the boot context above has already consumed a # number for the reboot.resume record, and seeding from the state here would # reissue it. $log = New-HDTLogContext -RunId ([string] $state.runId) -Phase FullOS -LogPath $logRoot ` -FileSystem $fileSystem -Clock $clock -Seq ([long] $bootLog.Seq) $catalog = New-HDTServiceCatalog -FileSystem $fileSystem -Clock $clock -Registry $registry ` -Lsa $lsa -Process $process -Power $power -ScriptInvoker $scriptInvoker -Cim $cim -Environment $environment $variable = [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($name in @($state.variable.Keys)) { $variable[[string] $name] = $state.variable[$name] } $context = New-HDTExecutionContext -RunId ([string] $state.runId) -Phase FullOS ` -WorkspaceRoot $WorkspaceRoot -Variable $variable -Service $catalog -Log $log -State $state $run = Invoke-HDTTaskSequence -Sequence $sequence -Context $context -State $state -StatePath $StatePath if ($run.Status -eq 'Failed') { exit 1 } exit 0 |