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 ', ') } |