Private/Progress.ps1
|
#Requires -Version 5.1 # What the commands people run by hand (Invoke-DoctorDOCollector, New-DoctorDODeviceReport, Test-DOConnectivity) say # while they run. Two layers: # # * For a person at the console (Test-DDConsoleNarration, 1.1.0): a header, one line per section as it finishes, with a # progress bar naming the section under way, then a summary (Console.ps1). On the Information stream, so # -InformationAction SilentlyContinue hides it; never under SYSTEM, Intune, a scheduled task or redirected output. # # DoctorDO.Collector 1.1.0: device report # PowerShell 7.6.8 (Core), not elevated, started 2026-10-23 09:44:15 UTC # # [1/8] Device and Windows ................... Microsoft Entra joined; Windows 10.0.26300; no ConfigMgr client # [6/8] Connectivity ......................... 65 checks: 39 pass, 2 warn, 2 fail, 18 not-applicable, 4 not-collected # # * With -Verbose: one line per step with the time since the start, what is happening and how it went, in aligned # columns. The header and the closing summary line are written only when nobody is narrated to (the narration has its # own). 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.1.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.2s] not collected: network.proxy.wininetScope ................ insufficient-rights # VERBOSE: ------------------------------------------------------------------------------------------------ # VERBOSE: [ 22.2s] Done ....................................................... report written, 752 KB # # The run (its clock, the command that owns it and whether anyone is watching) 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:DDSectionColumn = 44 $script:DDRule = '-' * 100 function Start-DDVerboseRun { <# .SYNOPSIS A new run, with its header, or the run of the command that called this one. Assign it to $DDRun. -Title and -Sections describe the run to a person at the console; a run of no sections (and a -Quiet one: the caller's -InformationAction SilentlyContinue) is narrated to nobody. #> [CmdletBinding()] param([Parameter(Mandatory)][string] $Command, [string] $Title = '', [int] $Sections = 0, [switch] $Quiet) $outer = Get-Variable -Name DDRun -ValueOnly -ErrorAction SilentlyContinue if ($null -ne $outer) { return $outer } $show = (-not $Quiet) -and $Sections -gt 0 -and (Test-DDConsoleNarration) $colour = $show -and (Test-DDConsoleColour) $run = [pscustomobject]@{ Command = $Command; Clock = [System.Diagnostics.Stopwatch]::StartNew(); Show = $show; Colour = $colour Title = $Title; Total = $Sections; Index = 0; Current = ''; Width = $(if ($show) { Get-DDConsoleWidth } else { 120 }) } if ($show -or $VerbosePreference -ne 'SilentlyContinue') { $rights = $(if (Test-DDSystemContext) { 'running as SYSTEM' } elseif (Test-DDElevated) { 'elevated' } else { 'not elevated' }) $started = [datetime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss', [System.Globalization.CultureInfo]::InvariantCulture) $platform = 'PowerShell {0} ({1}), {2}, started {3} UTC' -f $PSVersionTable.PSVersion, $PSVersionTable.PSEdition, $rights, $started if ($show) { Write-DDNarration (Format-DDColour -Text ('DoctorDO.Collector {0}: {1}' -f (Get-DDCollectorVersion), $Title) -Style bold -On $colour) Write-DDNarration (Format-DDColour -Text $platform -Style dim -On $colour) Write-DDNarration } else { Write-Verbose ('DoctorDO.Collector {0} - {1}' -f (Get-DDCollectorVersion), $Command) Write-Verbose $platform Write-Verbose $script:DDRule } } $run } function Write-DDNarration { # One line for a person at the console, on the Information stream with the tag DoctorDO, so 6>, -InformationVariable # and transcripts treat it like any other information. Callers check $DDRun.Show first. [CmdletBinding()] param([AllowEmptyString()][string] $Message = '') Write-Information -MessageData $Message -Tags 'DoctorDO' -InformationAction Continue } function Start-DDSection { # The next section of a narrated run; the progress bar names it while it runs. Nothing when nobody is watching. [CmdletBinding()] param([Parameter(Mandatory)][string] $Name) $run = Get-Variable -Name DDRun -ValueOnly -ErrorAction SilentlyContinue if ($null -eq $run -or -not $run.Show) { return } $run.Index++ $run.Current = $Name $percent = [int][math]::Min(99, 100 * ($run.Index - 1) / [math]::Max(1, $run.Total)) Write-Progress -Id 7680 -Activity ('DoctorDO.Collector: {0}' -f $run.Title) -Status ('[{0}/{1}] {2}' -f $run.Index, $run.Total, $Name) -PercentComplete $percent } function Complete-DDSection { # The line for the section that has just finished: its number, its name and how it went, the results in one column. # A scriptblock result is evaluated only when someone is watching, and a failure there never reaches the run. [CmdletBinding()] param($Result = 'done') $run = Get-Variable -Name DDRun -ValueOnly -ErrorAction SilentlyContinue if ($null -eq $run -or -not $run.Show -or -not $run.Current) { return } if ($Result -is [scriptblock]) { try { $Result = [string](& $Result) } catch { $Result = "(not described: $($_.Exception.Message))" } } $label = '[{0}/{1}] {2}' -f $run.Index, $run.Total, $run.Current $leader = '.' * [math]::Max(2, $script:DDSectionColumn - $label.Length - 1) $counter = '[{0}/{1}]' -f $run.Index, $run.Total Write-DDNarration (' {0} {1} {2} {3}' -f (Format-DDColour -Text $counter -Style dim -On $run.Colour), $run.Current, (Format-DDColour -Text $leader -Style dim -On $run.Colour), (Format-DDResultCounts -Text ([string]$Result) -On $run.Colour)) $run.Current = '' } 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 end of the run, only for the command that started it (not one that joined it): the progress bar is cleared for # a narrated run, else -Verbose gets the closing rule and how the run ended. [CmdletBinding()] param($Run, [Parameter(Mandatory)][string] $Command, [Parameter(Mandatory)][string] $Summary) if ($null -eq $Run -or $Run.Command -ne $Command) { return } if ($Run.Show) { Write-Progress -Id 7680 -Activity ('DoctorDO.Collector: {0}' -f $Run.Title) -Completed return } if ($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 ', ') } |