Public/Copy-HDTResumeAgent.ps1

function Copy-HDTResumeAgent {
    <#
        .SYNOPSIS
            Stages the engine and the resume payload onto the volume the machine
            is about to boot from.
 
        .DESCRIPTION
            THE HALF OF DESIGN 4.5.1 THAT WAS MISSING. The engine is "launched at
            logon by a RunOnce entry pointing at C:\HDT\Start-HDTResume.ps1" -
            and until this command existed, nothing ever put that file, or the
            module it imports, on the disk. Both were staged into the BOOT IMAGE
            at X:\HDT\, which is a RAM disk that ceases to exist at the restart.
 
            THE FAILURE THIS ENDS DID NOT LOOK LIKE A FAILURE. WinPE deployed
            Windows, the machine restarted, Windows autologged on - and then sat
            at a desktop with nothing to run. Every step in a FullOS group was
            dead, which on the shipped client template is the whole State Restore
            group, which is where applications install. A deployment that
            installs no software and reports success is worse than one that
            stops, because nobody goes looking.
 
            IT COPIES THE IMAGE'S OWN TREE, not a fresh one from the build host.
            The engine that resumes has to be the engine that started - a machine
            halfway through a task sequence is the worst possible place to change
            versions - and X:\HDT\Modules is by construction the copy the boot
            image was built with. This is also why it takes no -ModulePath: there
            is nothing to choose.
 
            WHAT TRAVELS, AND WHAT DOES NOT:
 
              Start-HDTResume.ps1 what RunOnce launches
              Modules\ the engine, and powershell-yaml, without
                                      which it can read no document at all
              bootstrap.json the deploy root and the account that opens
                                      it, so the full-OS leg can reach the share
                                      that holds Applications\
 
              Start-HDTDeployment.ps1 NOT staged - it is what startnet.cmd runs,
                                      and a copy on the deployed machine would be
                                      a second answer to "what starts a
                                      deployment", and the one nothing launches
              UI\ NOT staged - the wizard runs in WinPE,
                                      before this disk had a partition table
 
            AN EMPTY STAGE IS REFUSED, LOUDLY, and that is the whole point of the
            check: failing here means failing in WinPE, where the log is still
            being written and a technician is still watching. Succeeding into a
            desktop nobody is looking at is how this went unnoticed for five
            milestones.
 
            NOTHING HERE IS DELETED. The teardown (DESIGN 4.5.4) removes what
            this wrote, on the machine, at the end of the run.
 
        .PARAMETER TargetVolume
            The volume the operating system was applied to - the DRIVE LETTER
            HDTOSVolume carries. 'W', 'W:' and 'W:\' all mean the same volume
            and all stage to W:\HDT; anything that is not one letter is refused
            rather than turned into a relative path. On the booted machine the
            same folder is C:\HDT.
 
        .PARAMETER Source
            The boot image's own payload folder. X: is the only letter a WinPE
            payload may assume (SPIKES S9.1), and this is the one place it is
            assumed.
 
        .PARAMETER FileSystem
            An IFileSystem. Defaults to the real one.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject with Path, FileCount and
            Item.
 
        .EXAMPLE
            Copy-HDTResumeAgent -TargetVolume 'W:'
 
            Stages W:\HDT\ from X:\HDT\, which becomes C:\HDT\ when the machine
            boots.
 
        .LINK
            Set-HDTAutoLogon
 
        .LINK
            Invoke-HDTTaskSequence
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $TargetVolume,

        [Parameter(Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Source = 'X:\HDT',

        [Parameter()]
        [AllowNull()]
        [object] $FileSystem
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if ($null -eq $FileSystem) { $FileSystem = New-HDTFileSystem }

    if (-not $FileSystem.TestPath($Source)) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Source -Category ObjectNotFound `
                    -Message ("'{0}' is not there, so there is no engine to stage onto the deployed machine. This folder is what Update-HDTBootImage writes into the boot image; a machine running a payload from somewhere else must name it with -Source." -f $Source)))
    }

    $payload = [System.IO.Path]::Combine($Source, 'Start-HDTResume.ps1')

    if (-not $FileSystem.TestPath($payload)) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $payload -Category ObjectNotFound `
                    -Message ("'{0}' is not in this boot image, so the deployed machine would autologon with nothing to run and every step after the restart would be silently skipped. Rebuild the boot image with a version of Update-HDTBootImage that stages the resume payload." -f $payload)))
    }

    # A VOLUME IS A LETTER, AND HDTOSVolume CARRIES JUST THE LETTER - 'W', not
    # 'W:' and not 'W:\'. That is the convention every volume variable in this
    # engine follows, and Invoke-HDTApplyImageStep normalises exactly this way
    # before it builds a path from one.
    #
    # THE FIRST VERSION OF THIS TRIMMED SEPARATORS AND NOTHING ELSE, so the real
    # value composed 'W\HDT' - a RELATIVE path. On a live deployment 408 files
    # went onto the RAM disk, died with it, and the machine booted, autologged on
    # and had nothing to run. The unit tests were green throughout, because they
    # were written against 'W:' and 'W:\': two spellings the engine never
    # produces.
    $letter = ([string] $TargetVolume).Trim().TrimEnd('\', '/').TrimEnd(':')

    # ANYTHING THAT IS NOT ONE LETTER IS REFUSED RATHER THAN MADE RELATIVE. A
    # relative destination is the failure this command exists to prevent,
    # reached from the other end - files written somewhere nobody will look, and
    # a machine that boots into nothing.
    if ($letter -notmatch '^[A-Za-z]$') {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $TargetVolume -Category InvalidArgument `
                    -Message ("'{0}' is not a drive letter, so there is nowhere to stage the engine. HDTOSVolume carries the letter the partition step published; a value that is not one letter would compose a relative path and put the deployed machine's engine somewhere nothing will look for it." -f $TargetVolume)))
    }

    $destination = '{0}:\HDT' -f $letter.ToUpperInvariant()

    if (-not $PSCmdlet.ShouldProcess($destination, ("Stage the engine and the resume payload from '{0}'" -f $Source))) {
        return [pscustomobject] @{
            Path      = [string] $destination
            FileCount = 0
            Item      = [pscustomobject[]] @()
        }
    }

    $FileSystem.CreateDirectory($destination)

    $item = New-Object -TypeName System.Collections.ArrayList
    $count = 0

    # The module tree first, so a machine that dies mid-stage has no payload
    # standing over a missing engine - the one ordering that turns a failed
    # stage into a RunOnce entry that throws on Import-Module.
    $modules = [System.IO.Path]::Combine($Source, 'Modules')

    if ($FileSystem.TestPath($modules)) {
        $moduleCount = Copy-HDTContentTree -Source $modules `
            -Destination ([System.IO.Path]::Combine($destination, 'Modules')) -FileSystem $FileSystem

        $count += $moduleCount

        [void] $item.Add([pscustomobject] @{
                Name      = 'Modules'
                Source    = [string] $modules
                FileCount = [int] $moduleCount
            })
    }

    # bootstrap.json is optional: an image built for the Local provider has no
    # share to reach, and a full-OS leg of steps that need no content is a
    # legitimate deployment.
    foreach ($leaf in @('Start-HDTResume.ps1', 'bootstrap.json')) {
        $from = [System.IO.Path]::Combine($Source, $leaf)

        if (-not $FileSystem.TestPath($from)) { continue }

        $FileSystem.CopyItem($from, [System.IO.Path]::Combine($destination, $leaf))
        $count++

        [void] $item.Add([pscustomobject] @{
                Name      = [string] $leaf
                Source    = [string] $from
                FileCount = 1
            })
    }

    return [pscustomobject] @{
        Path      = [string] $destination
        FileCount = [int] $count
        Item      = [pscustomobject[]] @($item)
    }
}