Public/Get-HDTConsoleMonitor.ps1
|
function Get-HDTConsoleMonitor { <# .SYNOPSIS Which deployments are in flight on a share, and where each has got to. .DESCRIPTION The monitoring view: "tails Logs\_active\, showing in-flight deployments, current step, and elapsed time". The engine's heartbeat is the other half of the same decision - "The engine writes a small status.json heartbeat to <share>\Logs\_active\<RunId>.json each step. The console tails that directory. No web service, no SQL, no MDT Monitoring dependency." SO THE WHOLE FEATURE IS A DIRECTORY LISTING AND SOME ARITHMETIC, and both belong here rather than in the window. The console re-runs this on a timer and assigns what comes back; it works nothing out, which is what leaves the adapter exempt from TDD. THE CLOCK IS INJECTED. "How long since this said anything" is the one number on the screen that changes with nothing being written, and a command that read the wall clock could only be tested by sleeping. It is the ENGINE'S IClockService, so the console measures time the same way the deployment writing these files does. A HEARTBEAT THAT STOPPED IS THE POINT OF THE SCREEN. A deployment that died - power, network, a bugcheck - leaves its last heartbeat behind and never writes another. Nothing else in HDT will ever notice, because there is nothing left running to notice it. Health is therefore computed from the age of the file's own timestamp rather than taken from its status, which will still say Running. BUT A FINISHED RUN IS NOT A STALLED ONE, however old. Ageing a Succeeded heartbeat into a red row is how a screen learns to cry wolf, and a technician who has been trained to ignore red will ignore the one that mattered. THE THRESHOLD IS GENEROUS AND SETTABLE. The engine writes one per STEP and a step can legitimately take a long time - applying an image is minutes, and a driver pass on slow media is longer. Twenty minutes is the default because it is comfortably past the slowest ordinary step and comfortably short of a lunch break. MOST RECENT FIRST. A technician watching twenty machines wants the one that just moved at the top, not the one whose id sorts first. AN UNREADABLE HEARTBEAT IS STILL A RUN. A file caught mid-write, or truncated by a machine that lost power between the open and the flush, describes a deployment that exists; dropping the row would take the machine off the screen at the exact moment it most needs to be on it. Its id comes from the file name, which is the one thing still legible. .PARAMETER Path The deployment share's root. .PARAMETER StaleMinute How long a heartbeat may go unwritten before its run is called stalled. Twenty minutes by default. .PARAMETER FileSystem An IFileSystem. Defaults to the real one. .PARAMETER Clock An IClockService. Defaults to the real one. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject: Status, Message whether the directory could be read Run one row per heartbeat, most recent first Summary the line a person scans LiveCount, StalledCount, FinishedCount, UnreadableCount .EXAMPLE (Get-HDTConsoleMonitor -Path 'C:\HDTLab\Share').Run | Format-Table RunId, Phase, StepName, SinceText, Health .EXAMPLE Get-HDTConsoleMonitor -Path '\\192.168.2.108\HDTShare' -StaleMinute 45 #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter()] [ValidateRange(1, 1440)] [int] $StaleMinute = 20, [Parameter()] [AllowNull()] [object] $FileSystem, [Parameter()] [AllowNull()] [object] $Clock ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($null -eq $FileSystem) { $FileSystem = New-HDTFileSystem } if ($null -eq $Clock) { $Clock = New-HDTClock } $activePath = Join-Path -Path (Get-HDTWorkspacePath -Root $Path -Kind Logs) -ChildPath '_active' $now = $Clock.GetUtcNow().ToUniversalTime() $run = New-Object -TypeName System.Collections.ArrayList # A share that has never run a deployment has no such directory, and a # console that refused to open on it would be refusing on the commonest # share there is - a new one. $child = @() if ($FileSystem.TestPath($activePath)) { $child = @($FileSystem.GetChildItem($activePath)) } foreach ($file in $child) { if ([System.IO.Path]::GetExtension([string] $file) -ne '.json') { continue } $runId = [System.IO.Path]::GetFileNameWithoutExtension([string] $file) $document = $null try { $document = ConvertFrom-Json -InputObject ([string] $FileSystem.ReadAllText([string] $file)) } catch { $document = $null } [void] $run.Add((New-HDTConsoleMonitorRow -RunId $runId -Path ([string] $file) ` -Document $document -Now $now -StaleMinute $StaleMinute)) } # Most recent first. A row that could not be read has no timestamp of its # own, and sorts to the top - which is where a machine nobody can see the # state of belongs. $ordered = @($run | Sort-Object -Property @{ Expression = 'SinceSecond'; Descending = $false }) $live = @($ordered | Where-Object { $_.Health -eq 'Live' }) $stalled = @($ordered | Where-Object { $_.Health -eq 'Stalled' }) $finished = @($ordered | Where-Object { $_.Health -eq 'Finished' }) $unreadable = @($ordered | Where-Object { $_.Health -eq 'Unreadable' }) return [pscustomobject] @{ Status = 'Ok' Message = '' Run = [pscustomobject[]] @($ordered) Summary = (Get-HDTConsoleMonitorSummary -Live @($live).Count -Stalled @($stalled).Count ` -Finished @($finished).Count -Unreadable @($unreadable).Count) # The same fact in the form a tree row wears it - see the -Caption note # on Get-HDTConsoleMonitorSummary for why the quiet case is not a # sentence in brackets. Caption = (Get-HDTConsoleMonitorSummary -Live @($live).Count -Stalled @($stalled).Count ` -Finished @($finished).Count -Unreadable @($unreadable).Count -Caption) LiveCount = @($live).Count StalledCount = @($stalled).Count FinishedCount = @($finished).Count UnreadableCount = @($unreadable).Count ActivePath = $activePath } } |