Public/New-HDTServiceCatalog.ps1
|
function New-HDTServiceCatalog { <# .SYNOPSIS Builds the injected service catalog a step reaches the outside world through. .DESCRIPTION The injection rule in one object: "a step implementation may not call DISM, CIM, the filesystem or the network directly - it receives those through injected service objects". The catalog is the single thing a step is handed, and the single thing a test replaces to prove the step with no machine attached. FileSystem IFileSystem (mandatory) Clock IClock (mandatory) Registry IRegistryService Lsa ILsaService Process IProcessService Power IPowerService ScriptInvoker IScriptInvoker Cim ICimProvider Environment IEnvironmentProvider Disk IDiskService Image IImageService Feature IFeatureService BitLocker IBitLockerService Content IContentProvider EVERY PROPERTY IS DEFINED EVEN WHERE IT IS $null. Engine code runs under Set-StrictMode -Version Latest, where reading a property that was never defined throws "The property 'Process' cannot be found on this object" - an error that says nothing about which step wanted what. GetRequired REPLACES THAT WITH A SENTENCE. $Context.Service.GetRequired('Process', 'CommandLine') returns the service, or throws naming both the missing service and the step type that asked for it. A step author writes one call and gets a message an administrator can act on. FileSystem and Clock are mandatory and nothing else is, because a NoOp sequence must be runnable with two services and no more. It is a [pscustomobject] carrying ScriptMethod members rather than a PowerShell class: classes dot-sourced into the module are the known flaky path across -Force re-imports (see 01-03). .PARAMETER FileSystem An IFileSystem. Mandatory. .PARAMETER Clock An IClock. Mandatory. .PARAMETER Registry An IRegistryService, or nothing. .PARAMETER Lsa An ILsaService, or nothing. .PARAMETER Process An IProcessService, or nothing. .PARAMETER Power An IPowerService, or nothing. .PARAMETER ScriptInvoker An IScriptInvoker, or nothing. .PARAMETER Cim An ICimProvider, or nothing. .PARAMETER Environment An IEnvironmentProvider, or nothing. .PARAMETER Disk An IDiskService, or nothing. DiskPartition asks for it by name. .PARAMETER Image An IImageService, or nothing. ApplyImage and ConfigureBoot ask for it by name. .PARAMETER Feature An IFeatureService, or nothing. InstallRoles asks for it by name. .PARAMETER BitLocker An IBitLockerService, or nothing. EnableBitLocker asks for it by name. .PARAMETER Content An IContentProvider, or nothing - New-HDTLocalContentProvider or New-HDTSmbContentProvider. ApplyImage resolves the catalog's image through it when the run was started with one, which is the whole of "a provider swap, not a parallel code path" as far as a step is concerned. .OUTPUTS System.Management.Automation.PSCustomObject with the twelve service properties and a GetRequired ScriptMethod. .EXAMPLE $catalog = New-HDTServiceCatalog -FileSystem (New-HDTFileSystem) -Clock (New-HDTClock) The smallest catalog that runs a NoOp sequence. .EXAMPLE $catalog = New-HDTServiceCatalog -FileSystem $fs -Clock $clock -Process (New-HDTProcessService) $catalog.GetRequired('Process', 'CommandLine') #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Builds a stateless container of service adapters; it changes no state.')] [CmdletBinding()] [OutputType([object])] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $FileSystem, [Parameter(Mandatory = $true)] [ValidateNotNull()] [object] $Clock, [Parameter()] [AllowNull()] [object] $Registry = $null, [Parameter()] [AllowNull()] [object] $Lsa = $null, [Parameter()] [AllowNull()] [object] $Process = $null, [Parameter()] [AllowNull()] [object] $Power = $null, [Parameter()] [AllowNull()] [object] $ScriptInvoker = $null, [Parameter()] [AllowNull()] [object] $Cim = $null, [Parameter()] [AllowNull()] [object] $Environment = $null, [Parameter()] [AllowNull()] [object] $Disk = $null, [Parameter()] [AllowNull()] [object] $Image = $null, [Parameter()] [AllowNull()] [object] $Feature = $null, [Parameter()] [AllowNull()] [object] $BitLocker = $null, [Parameter()] [AllowNull()] [object] $Content = $null, # NULL IS THE NORMAL CASE, AND IT COSTS NOTHING. A run with no progress # display never reads its own log to draw one - see # Update-HDTProgressDisplay. DESIGN 11.1's HDTSkipProgress and every # sequence test in the suite arrive here as $null. [Parameter()] [AllowNull()] [object] $Progress = $null ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $catalog = [pscustomobject] @{ FileSystem = $FileSystem Clock = $Clock Registry = $Registry Lsa = $Lsa Process = $Process Power = $Power ScriptInvoker = $ScriptInvoker Cim = $Cim Environment = $Environment Disk = $Disk Image = $Image Feature = $Feature BitLocker = $BitLocker Content = $Content Progress = $Progress } $catalog | Add-Member -MemberType ScriptMethod -Name GetRequired -Value { param([string] $Name, [string] $Caller) $asked = 'a step' if (-not [string]::IsNullOrWhiteSpace($Caller)) { $asked = "the {0} step" -f $Caller } $known = @($this.PSObject.Properties | Where-Object { $_.MemberType -eq 'NoteProperty' } | ForEach-Object { $_.Name }) if ($known -notcontains $Name) { throw ("{0} asked the service catalog for '{1}', which is not a service HDT carries. The services are {2}." -f $asked, $Name, ($known -join ', ')) } $service = $this.$Name if ($null -eq $service) { throw ("{0} needs the {1} service, but the run was started without one. Add it to New-HDTServiceCatalog." -f $asked, $Name) } return $service } return $catalog } |