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() # A card is as wide as the window: a row says what a Check found, and half of that is # no use to anybody. And two thirds of its height at most, so that the banner and the # parts that are done stay in sight above the Checks. Set-LanternOption -Card Accordion -Banner Sweep -Progress Hairline -CardWidth 0 -CardHeight 67 -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 Get-ConsoleCardRoom { <# .SYNOPSIS How many columns there are inside a card for what it holds, in this window. .DESCRIPTION For what must not be cut or wrapped, which is the QR code: it is drawn only where all of it fits. A card is as wide as the window, less the column every Frame leaves free, and has two columns of border and blank on either side. Like Test-ConsoleLive, this reads what Lantern's own functions read. #> [CmdletBinding()] [OutputType([int])] param() try { [int](Resolve-LanternTerminal -Terminal $null).Width - 1 - 4 } catch { 0 } } 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-ConsoleHeading { <# .SYNOPSIS Says which part of a Run begins, where lines scroll past. A card has its title for that, and nothing is written. #> [CmdletBinding()] [OutputType([void])] param([Parameter(Position = 0)][AllowEmptyString()][string]$Text = '', [AllowEmptyString()][string]$Color = '') if ($script:ConsolePart) { return } Write-LanternLine -Text $Text -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 { <# .SYNOPSIS What a Run shows of itself before anything else: its face, its name and its version. .DESCRIPTION Two drawings of the same face. Where the window has the glyphs for it, a small screen in thin rounded lines with two eyes and a mouth, and the version beside the name. Where it has ASCII only - Windows PowerShell 5.1 in a classic console among them - the screen it always was. Lantern knows which, and this asks what its own functions ask. The glyphs are written as their numbers: this file has no byte order mark, and Windows PowerShell would read them as something else. #> [CmdletBinding()] param($Version) $drawn = $false try { $drawn = (Get-LanternLook -Terminal (Resolve-LanternTerminal -Terminal $null)).Glyph.EdgeDown -ne '|' } catch { } if ($drawn) { $across = [string][char]0x2500 $down = [string][char]0x2502 $top = [string][char]0x256D + ($across * 10) + [string][char]0x256E $bottom = [string][char]0x2570 + ($across * 10) + [string][char]0x256F # The face, and the face with its eyes shut: once it is there, it blinks. $face = '{0} {1} {2} {1} {0}' -f $down, '{0}', [string][char]0x203F Show-LanternBanner -Framed -Art @($top, ($face -f [string][char]0x25CF), $bottom) ` -Blink @($top, ($face -f $across), $bottom) -Text @( ('gutcheck v{0}' -f $Version) (Get-Text 'Console.Banner.Tagline1') (Get-Text 'Console.Banner.Tagline2') ) Write-LanternLine '' return } # 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 ||' '|| ____ ||' '|| ||' "'================'" ) -Blink @( '.================.' '|| - - ||' '|| - - ||' '|| ____ ||' '|| ||' "'================'" ) -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: its start, the Check Definitions, the Scope, the Checks or the Result. Nothing where the window cannot draw. #> [CmdletBinding()] [OutputType([void])] param([Parameter(Mandatory)][ValidateSet('Start', 'Definitions', 'Scope', 'Checks', 'Result')][string]$Part) if (-not (Test-ConsoleLive)) { return } # Open already: the Result is opened when the Checks are done, and again by what # writes into it. if ($script:ConsolePart -eq $Part) { return } $script:ConsolePart = $Part $titles = @( (Get-Text 'Console.Part.Start'), (Get-Text 'Console.Part.Definitions'), (Get-Text 'Console.Part.Scope'), (Get-Text 'Console.Part.Checks'), (Get-Text 'Console.Part.Result') ) Start-LanternCard -Title $titles[@('Start', 'Definitions', '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-ResultProgress { <# .SYNOPSIS The bar of the Result card: which of the steps that make the Report a Run is at. Nothing where the Result is no card. .DESCRIPTION Between the last Check and the Report there are some seconds in which the Findings are put together and written, and a window that shows nothing in them looks as if the Run had stopped. Counted in steps, because that is all that is known of how long each takes. #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)][string]$Status, [Parameter(Mandatory)][int]$Step, [int]$Of = 3 ) if ($script:ConsolePart -ne 'Result') { return } Write-LanternProgress (Get-Text 'Console.Result.Bar') -Status $Status -Step $Step -Of $Of } 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] } if ($script:ConsolePart) { # The bar is full, and what it leaves in the card when it goes is where the Report # is: the last line of the Run, as it is where lines scroll past. Write-LanternProgress (Get-Text 'Console.Result.Bar') -PercentComplete 100 Complete-LanternProgress -Status $ReportPath } else { Write-ConsoleLine (Get-Text 'Console.Summary.Report' $ReportPath) -Color Cyan } Complete-RunPart -Open } |