Private/Progress.ps1

#Requires -Version 5.1
# -Verbose progress for the commands people run by hand (Invoke-DoctorDOCollector, New-DoctorDODeviceReport,
# Test-DOConnectivity): a header, then one line per step with the time since the start, what is happening and how it
# went, in aligned columns, then a summary. Write-Verbose only, so nothing is printed without -Verbose. The lines name no
# host, user or address: a verbose transcript can be pasted into a support request as it is.
#
# VERBOSE: DoctorDO.Collector 1.0.0 - New-DoctorDODeviceReport
# VERBOSE: PowerShell 7.6.6 (Core), elevated, started 2026-10-15 17:30:12 UTC
# VERBOSE: ------------------------------------------------------------------------------------------------
# VERBOSE: [ 0.8s] Device identity (dsregcmd) ................................. join type hybrid
# VERBOSE: [ 21.1s] Endpoint health ............................................ 27 results: 24 pass, 1 warn, 2 fail
# VERBOSE: [ 21.2s] not collected: network.proxy.wininetScope ................ insufficient-rights
# VERBOSE: ------------------------------------------------------------------------------------------------
# VERBOSE: [ 22.2s] Done ....................................................... report written, 752 KB
#
# The run (its clock and the command that owns it) is the $DDRun variable of the public command, which every function it
# calls sees through PowerShell's dynamic scoping: nothing is kept in the module between calls, and a command called
# from another one (Test-DOConnectivity calling Get-DOConnectivityMatrix) joins the run already under way.

Set-StrictMode -Version Latest

$script:DDStepColumn = 60
$script:DDRule = '-' * 100

function Start-DDVerboseRun {
    # A new run, with its header, or the run of the command that called this one. Assign it to $DDRun.
    [CmdletBinding()]
    param([Parameter(Mandatory)][string] $Command)
    $outer = Get-Variable -Name DDRun -ValueOnly -ErrorAction SilentlyContinue
    if ($null -ne $outer) { return $outer }
    $run = [pscustomobject]@{ Command = $Command; Clock = [System.Diagnostics.Stopwatch]::StartNew() }
    if ($VerbosePreference -ne 'SilentlyContinue') {
        $rights = $(if (Test-DDSystemContext) { 'running as SYSTEM' } elseif (Test-DDElevated) { 'elevated' } else { 'not elevated' })
        Write-Verbose ('DoctorDO.Collector {0} - {1}' -f (Get-DDCollectorVersion), $Command)
        Write-Verbose ('PowerShell {0} ({1}), {2}, started {3} UTC' -f $PSVersionTable.PSVersion, $PSVersionTable.PSEdition, $rights,
            [datetime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss', [System.Globalization.CultureInfo]::InvariantCulture))
        Write-Verbose $script:DDRule
    }
    $run
}

function Write-DDVerboseStep {
    # One step: the seconds since the run started, the step, a dotted leader to a fixed column, then the result. A
    # scriptblock result is evaluated only with -Verbose, and a failure there never reaches the run: describing a step
    # must not be able to break it.
    [CmdletBinding()]
    param([Parameter(Mandatory)][string] $Step, $Result = 'done')
    if ($VerbosePreference -eq 'SilentlyContinue') { return }
    if ($Result -is [scriptblock]) {
        try { $Result = [string](& $Result) } catch { $Result = "(not described: $($_.Exception.Message))" }
    }
    $run = Get-Variable -Name DDRun -ValueOnly -ErrorAction SilentlyContinue
    $time = $(if ($null -ne $run) { $run.Clock.Elapsed.TotalSeconds.ToString('0.0', [System.Globalization.CultureInfo]::InvariantCulture) + 's' } else { '' })
    $leader = '.' * [math]::Max(2, $script:DDStepColumn - $Step.Length - 1)
    Write-Verbose ('[{0,7}] {1} {2} {3}' -f $time, $Step, $leader, $Result)
}

function Stop-DDVerboseRun {
    # The closing rule and how the run ended; only for the command that started the run, not one that joined it.
    [CmdletBinding()]
    param($Run, [Parameter(Mandatory)][string] $Command, [Parameter(Mandatory)][string] $Summary)
    if ($null -eq $Run -or $Run.Command -ne $Command -or $VerbosePreference -eq 'SilentlyContinue') { return }
    Write-Verbose $script:DDRule
    Write-DDVerboseStep -Step 'Done' -Result $Summary
}

function Get-DDCountText {
    # '27 results: 24 pass, 1 warn, 2 fail' from connectivity rows, naming only the results that occur.
    [CmdletBinding()]
    param([AllowEmptyCollection()] $Rows = @(), [string] $Noun = 'results')
    $list = @($Rows | Where-Object { $_ -is [System.Collections.IDictionary] })
    if ($list.Count -eq 0) { return "no $Noun" }
    $parts = foreach ($r in 'pass', 'warn', 'fail', 'not-applicable', 'not-collected') {
        $n = @($list | Where-Object { $_['result'] -eq $r }).Count
        if ($n) { '{0} {1}' -f $n, $r }
    }
    '{0} {1}: {2}' -f $list.Count, $Noun, (@($parts) -join ', ')
}