Private/Console.ps1
|
# What a Technician sees while a Run is happening. # # The Report is the product of a Run; this is only the running commentary, captured into # console.log by the transcript. Nothing here decides anything, and no Check calls it: # Findings are returned as values and the Run displays them. # One Severity, one colour, in one place. The Report renders the same decision as CSS # classes; these are the console's half of it. $script:SeverityColour = @{ OK = 'Green'; INFO = 'Gray'; WARN = 'Yellow'; FAIL = 'Red' } # The console's own switches, as Windows numbers them (SetConsoleMode, on the input handle). $script:ConsoleStdInputHandle = -10 $script:ConsoleQuickEditMode = 0x0040 $script:ConsoleExtendedFlags = 0x0080 function Initialize-ConsoleNativeMethod { <# .SYNOPSIS Loads the three Windows calls that read and set a console's mode. Once, on first use. .DESCRIPTION Not at import: importing this module must do nothing but define functions. #> [CmdletBinding()] param() if ('Gutcheck.ConsoleNative' -as [type]) { return } Add-Type -Namespace Gutcheck -Name ConsoleNative -MemberDefinition @' [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr GetStdHandle(int nStdHandle); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); '@ } function Get-ConsoleModeWithoutQuickEdit { <# .SYNOPSIS A console mode with QuickEdit taken out, and nothing else about it changed. Pure. .DESCRIPTION Windows only honours a change to QuickEdit when the extended-flags bit is set in the same call, so that bit goes in with it. #> [CmdletBinding()] [OutputType([uint32])] param([Parameter(Mandatory)][uint32]$Mode) [uint32](($Mode -band (-bnot [uint32]$script:ConsoleQuickEditMode)) -bor $script:ConsoleExtendedFlags) } function Disable-ConsoleQuickEdit { <# .SYNOPSIS Stops a click in the console window from freezing the Run. Returns what the mode was, for Restore-ConsoleMode, or $null when there was nothing to change. .DESCRIPTION In a classic console window a click starts a text selection, and while one is active Windows blocks every program that writes to the window. The program stands still at its next line of output until somebody presses Enter. The Elevated Part runs in such a window, and one click on it - to bring it to the front, to see what it is - left the Main Part waiting out its deadline for a process that was fine. With QuickEdit off the window can still be read, scrolled and marked from its menu; it just no longer stops what is running in it. Windows Terminal never had the problem and ignores the switch. Never throws: no console, a redirected input, a host that is no console at all - each is a Run with no window to click on. #> [CmdletBinding()] param() try { Initialize-ConsoleNativeMethod $handle = [Gutcheck.ConsoleNative]::GetStdHandle($script:ConsoleStdInputHandle) [uint32]$mode = 0 if (-not [Gutcheck.ConsoleNative]::GetConsoleMode($handle, [ref]$mode)) { return $null } $wanted = Get-ConsoleModeWithoutQuickEdit -Mode $mode if ($wanted -eq $mode) { return $null } if (-not [Gutcheck.ConsoleNative]::SetConsoleMode($handle, $wanted)) { return $null } $mode } catch { $null } } function Restore-ConsoleMode { <# .SYNOPSIS Puts a console back the way Disable-ConsoleQuickEdit found it. Never throws. #> [CmdletBinding()] param([AllowNull()]$Mode) if ($null -eq $Mode) { return } try { Initialize-ConsoleNativeMethod $handle = [Gutcheck.ConsoleNative]::GetStdHandle($script:ConsoleStdInputHandle) $null = [Gutcheck.ConsoleNative]::SetConsoleMode($handle, [uint32]$Mode) } catch { } } function Initialize-Console { <# .SYNOPSIS Tells Lantern how a Run looks and what it says: German, the parts of a Run folding to a line each, and the banner brought in by a light running across it. .DESCRIPTION Lantern is what asks a Run's questions and draws its cards (ADR-0007). It keeps what it is told here in variables of this module and does nothing else: nothing is drawn, asked or written. #> [CmdletBinding()] param() Set-LanternOption -Card Accordion -Banner Sweep -Progress Hairline -Text @{ Yes = Get-Text 'Console.Ask.Yes' No = Get-Text 'Console.Ask.No' None = Get-Text 'Console.Ask.None' Cancelled = Get-Text 'Console.Ask.Cancelled' Done = Get-Text 'Console.Ask.Done' Selected = Get-Text 'Console.Ask.Selected' Filter = Get-Text 'Console.Ask.Filter' NoMatch = Get-Text 'Console.Ask.NoMatch' Add = Get-Text 'Console.Ask.Add' Range = Get-Text 'Console.Ask.Range' HintChoice = Get-Text 'Console.Ask.HintChoice' HintMultiChoice = Get-Text 'Console.Ask.HintMultiChoice' PlainChoice = Get-Text 'Console.Ask.PlainChoice' PlainMultiChoice = Get-Text 'Console.Ask.PlainMultiChoice' PlainSelection = Get-Text 'Console.Ask.PlainSelection' PlainAdded = Get-Text 'Console.Ask.PlainAdded' PlainDefault = Get-Text 'Console.Ask.PlainDefault' PlainInvalid = Get-Text 'Console.Ask.PlainInvalid' } } # The card that is open, by its key, or nothing. Only ever set where the window can draw. $script:ConsolePart = $null function Test-ConsoleLive { <# .SYNOPSIS Whether the window of this Run can be drawn in: redrawn in place, as cards. .DESCRIPTION Where it can, a Run shows its parts as cards and each Check as a row of one. Where it cannot - Windows PowerShell 5.1, a redirected output, a host that is no console - a Run writes what it always wrote, line after line, and console.log has every Finding. Lantern decides this and has no question to ask it by. This reads what its own functions read, and is the one place Gutcheck looks inside it. #> [CmdletBinding()] [OutputType([bool])] param() try { [bool](Resolve-LanternTerminal -Terminal $null).LiveOutput } catch { $false } } function Write-ConsoleLine { <# .SYNOPSIS Says one thing to the Technician: a row of the card that is open, or a line. .DESCRIPTION What Write-Host is everywhere else. Anything said while a card may be open goes through here, because a line written past Lantern lands in the middle of the card. #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Position = 0)][AllowEmptyString()][AllowNull()][string]$Text = '', [AllowEmptyString()][string]$Color = '' ) if (-not $script:ConsolePart) { Write-LanternLine -Text $Text -Color $Color return } # The texts carry the blank lines and the indents that set them apart in a scrolling # window. A card has its border for that, and a line break inside a row would break # the border. foreach ($line in @("$Text" -split "\r?\n" | Where-Object { $_.Trim() })) { Write-LanternLine -Text $line.Trim() -Color $Color } } function Write-ConsoleNote { <# .SYNOPSIS Says what a Check is doing just now, and why the Technician is waiting: a line where lines scroll past, and in a card the text of the row that is going. .DESCRIPTION A line stays where it was written. In a card it would stay too, under every row, and still say "waiting for the admin part" when the last Check is done. There it is what the row says until the row says something else. #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Position = 0)][AllowEmptyString()][AllowNull()][string]$Text = '', [AllowEmptyString()][string]$Color = '' ) if (-not $script:ConsolePart) { Write-LanternLine -Text $Text -Color $Color; return } if (-not $script:ProgressRow) { return } $said = @("$Text" -split "\r?\n" | Where-Object { $_.Trim() }) if ($said.Count) { Write-LanternCardRow -Name $script:ProgressRow -Text $said[0].Trim().Trim('=', '.').Trim() } } function Show-Banner { [CmdletBinding()] param($Version) # The words stand beside the art, line for line, and are not written into it: the # art is fixed width, and a tagline and a version number are not. Show-LanternBanner -Framed -Art @( '.================.' '|| - - ||' '|| o o ||' '|| ____ ||' '|| ||' "'================'" ) -Text @( '' 'gutcheck' '' (Get-Text 'Console.Banner.Tagline1') (Get-Text 'Console.Banner.Tagline2') ('v{0}' -f $Version) ) Write-LanternLine '' } function Start-RunPart { <# .SYNOPSIS Opens the card of one part of a Run: Scope, Checks or Result. Nothing where the window cannot draw. #> [CmdletBinding()] [OutputType([void])] param([Parameter(Mandatory)][ValidateSet('Scope', 'Checks', 'Result')][string]$Part) if (-not (Test-ConsoleLive)) { return } $script:ConsolePart = $Part $titles = @((Get-Text 'Console.Part.Scope'), (Get-Text 'Console.Part.Checks'), (Get-Text 'Console.Part.Result')) Start-LanternCard -Title $titles[@('Scope', 'Checks', 'Result').IndexOf($Part)] -Of $titles } function Complete-RunPart { <# .SYNOPSIS Closes the card that is open, with what came of that part. Never throws, and does nothing when none is open. .PARAMETER Open Leaves the card as it is instead of folding it to a line: for the Result, whose rows are what the Technician is left with. #> [CmdletBinding()] [OutputType([void])] param( [AllowEmptyString()][string]$Status = '', [ValidateSet('OK', 'INFO', 'WARN', 'FAIL')][string]$Severity = 'OK', [switch]$Open ) if (-not $script:ConsolePart) { return } $script:ConsolePart = $null try { Complete-LanternCard -Status $Status -Mark $script:SeverityMark[$Severity] -Open:$Open } catch { } } # One Severity, one mark of a row, the same way as one colour. $script:SeverityMark = @{ OK = 'Ok'; INFO = 'Info'; WARN = 'Warn'; FAIL = 'Fail' } function Show-CheckRow { <# .SYNOPSIS Lists what the Checks card is going to do, each as a row still to come, so that a Technician sees how much there is. Nothing where no card is open. #> [CmdletBinding()] [OutputType([void])] param([AllowNull()][AllowEmptyCollection()][string[]]$Name) if (-not $script:ConsolePart) { return } foreach ($one in @($Name | Where-Object { $_ })) { Write-LanternCardRow -Name $one -Mark Todo } } function Complete-RunRow { <# .SYNOPSIS Says how a row of the open card ended. Nothing where no card is open. #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)][string]$Name, [ValidateSet('OK', 'INFO', 'WARN', 'FAIL')][string]$Severity = 'OK', [AllowEmptyString()][AllowNull()][string]$Text = '' ) if (-not $script:ConsolePart) { return } Write-LanternCardRow -Name $Name -Mark $script:SeverityMark[$Severity] -Text (("$Text" -split "\r?\n")[0]) } function Write-CheckHeading { <# .SYNOPSIS Says which Check begins. In a card the Check has its row, and nothing is written. #> [CmdletBinding()] [OutputType([void])] param([Parameter(Mandatory)][string]$Name) if ($script:ConsolePart) { return } Write-Host ("`n== {0} ==" -f $Name) -ForegroundColor Cyan } function Write-CheckResult { <# .SYNOPSIS Shows what a Check found: as its row in the Checks card, or as a line for every Finding where the window cannot draw. .DESCRIPTION A row has room for one thing, so it says the worst of what the Check found, and how many Findings there are beside it. The others are in the Report, and those that matter are in the Result card. Where nothing is drawn every Finding is a line, as it always was, and console.log has them all. #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)][string]$Name, [AllowNull()][AllowEmptyCollection()]$Finding ) $found = @($Finding | Where-Object { $_ }) if (-not $script:ConsolePart) { foreach ($one in $found) { Write-FindingToHost -Finding $one } return } if (-not $found.Count) { Complete-RunRow -Name $Name -Severity INFO; return } # A Finding has a name of its own beside the Check's, and its value says little # without it. $worst = @(Sort-Finding -Finding $found)[0] $text = '{0}: {1}' -f $worst.Check, $worst.Value if ($found.Count -gt 1) { $text = (Get-Text 'Console.Part.More') -f $text, ($found.Count - 1) } Complete-RunRow -Name $Name -Severity $worst.Severity -Text $text } function Write-FindingToHost { [CmdletBinding()] param([Parameter(Mandatory)]$Finding) Write-Host (' [{0,-4}] {1}: {2}' -f $Finding.Severity, $Finding.Check, $Finding.Value) ` -ForegroundColor $script:SeverityColour[$Finding.Severity] } function Write-WorstFindingsToHost { <# .SYNOPSIS Repeats the Findings that matter, worst first, once a Run has finished. .DESCRIPTION A Technician watching a Run needs the FAILs and WARNs gathered in one place at the end. This is not the Report; it is a pointer to it. Where the window can draw it is the Result card, and the one card that stays open: the Checks have folded to a line by then, and this is where what they found is left to read. #> [CmdletBinding()] param( [AllowNull()][AllowEmptyCollection()]$Finding, [Parameter(Mandatory)][string]$ReportPath ) $failed = @($Finding | Where-Object { $_.Severity -eq 'FAIL' }).Count $warned = @($Finding | Where-Object { $_.Severity -eq 'WARN' }).Count Start-RunPart -Part Result # In the card its title says what this is, and the Checks card above it the counts. if (-not $script:ConsolePart) { Write-Host (Get-Text 'Console.Summary.Heading' $failed $warned) -ForegroundColor Cyan } Sort-Finding -Finding $Finding | Where-Object { $_.Severity -eq 'FAIL' -or $_.Severity -eq 'WARN' } | ForEach-Object { Write-ConsoleLine (' [{0}] {1}: {2}' -f $_.Severity, $_.Check, $_.Value) -Color $script:SeverityColour[$_.Severity] } Write-ConsoleLine (Get-Text 'Console.Summary.Report' $ReportPath) -Color Cyan Complete-RunPart -Open } |