Private/Kinds/Printing.ps1
|
# The Printing Kind: the print spooler, the default printer, and what is stuck in between. # # Printing is where "Word is slow" most often turns out not to be Word. Office asks the # default printer for its capabilities when a document opens, so a default printer on a # network that is no longer there - the office printer, from home - holds every document # open for as long as the connection takes to time out. A spooler crashing on a bad driver # takes every printer with it, and one job stuck at the head of a queue blocks the rest. # # Gathered in the Technician's own session, never the Elevated Part: printer connections # and the default printer belong to the user, and an administrator's session would see a # different set. # How often the default printer is connected to. Not a Check Definition parameter, for the # reason Private/Sampling.ps1 gives about measurements: one connect is not evidence, and a # Customer may disagree about a threshold, not about how many attempts make a reading. $script:PrintingConnectAttempts = 3 # Where the print system records what went wrong. Its Admin channel is enabled on every # Windows and readable without admin rights. $script:PrintingLog = 'Microsoft-Windows-PrintService/Admin' # The port a printer shared from a print server is reached through. $script:PrintingServerPort = 445 function Get-PrintingData { [CmdletBinding()] [OutputType([psobject])] param([hashtable]$Parameters = @{}) $days = [int](Get-Parameter $Parameters 'Days' 30) $since = (Get-Date).AddDays(-$days) $spooler = Get-CimInstance Win32_Service -Filter "Name='Spooler'" -ErrorAction SilentlyContinue | Select-Object -First 1 $printers = @(Get-CimInstance Win32_Printer -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)" Default = [bool]$_.Default Network = [bool]$_.Network WorkOffline = [bool]$_.WorkOffline PrinterStatus = $_.PrinterStatus ExtendedPrinterStatus = $_.ExtendedPrinterStatus DetectedErrorState = $_.DetectedErrorState PortName = "$($_.PortName)" ServerName = "$($_.ServerName)" ShareName = "$($_.ShareName)" DriverName = "$($_.DriverName)" } }) $ports = @(Get-CimInstance Win32_TCPIPPrinterPort -ErrorAction SilentlyContinue | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)"; HostAddress = "$($_.HostAddress)"; PortNumber = $_.PortNumber } }) $default = $printers | Where-Object { $_.Default } | Select-Object -First 1 $reach = $null if ($default) { $reach = Measure-PrinterReachability -Printer $default -Port $ports } $now = Get-Date $jobs = @(Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue | ForEach-Object { $age = $null if ($_.TimeSubmitted -is [datetime]) { $age = [math]::Round(($now - $_.TimeSubmitted).TotalMinutes, 1) } [pscustomobject]@{ # Win32_PrintJob names a job "<printer>, <id>"; the printer is what matters. Printer = ("$($_.Name)" -replace ',\s*\d+$', '') Document = "$($_.Document)" JobStatus = "$($_.JobStatus)" AgeMinutes = $age } }) $refused = @{} $errors = @() if (Test-EventLogReadable -Log $script:PrintingLog -Unreadable $refused) { try { # Level 1 and 2 are Critical and Error. The Admin channel logs little else, but # the warnings it does log are about drivers being replaced, which is no problem. $errors = @(Get-WinEvent -FilterHashtable @{ LogName = $script:PrintingLog; Level = 1, 2; StartTime = $since } -ErrorAction Stop | ForEach-Object { ConvertTo-EventRow -LogEntry $_ -Tag 'Print error' }) } catch { } # "No events matched" arrives as an error too; the log was readable. } $unreadable = $refused.ContainsKey($script:PrintingLog) [pscustomobject]@{ PSTypeName = 'Gutcheck.Data.Printing' Days = $days SpoolerFound = $null -ne $spooler SpoolerState = "$($spooler.State)" SpoolerMode = "$($spooler.StartMode)" Printers = $printers Reachability = $reach Jobs = $jobs Errors = $errors LogUnreadable = $unreadable } } function Measure-PrinterReachability { <# .SYNOPSIS Connects to whatever the default printer is reached through, if anything. .DESCRIPTION A printer shared from a print server is reached through the server, over SMB. A printer with its own TCP/IP port is reached directly, on that port. Anything else - USB, WSD, a PDF writer - has no address this can connect to, and says so rather than reporting a connection nobody made. #> [CmdletBinding()] [OutputType([psobject])] param( [Parameter(Mandatory)]$Printer, [AllowEmptyCollection()][object[]]$Port = @() ) $hostName = $null $number = $null $via = 'None' if ($Printer.ServerName) { $hostName = $Printer.ServerName.TrimStart('\') $number = $script:PrintingServerPort $via = 'PrintServer' } else { $tcp = @($Port | Where-Object { $_.Name -eq $Printer.PortName }) | Select-Object -First 1 if ($tcp -and $tcp.HostAddress) { $hostName = $tcp.HostAddress $number = [int]$tcp.PortNumber $via = 'TcpPort' } } if (-not $hostName) { return [pscustomobject]@{ Via = $via; HostName = $null; Port = $null; Resolved = $null; Attempts = 0; Successes = 0; AverageMs = $null } } $resolved = $true if ($hostName -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { $resolved = [bool](Resolve-HostAddress -HostName $hostName).Resolved } $times = @() if ($resolved) { $times = @(1..$script:PrintingConnectAttempts | ForEach-Object { Measure-TcpConnect -HostName $hostName -Port $number } | Where-Object { $null -ne $_ }) } [pscustomobject]@{ Via = $via HostName = $hostName Port = $number Resolved = $resolved Attempts = $script:PrintingConnectAttempts Successes = $times.Count AverageMs = $(if ($times.Count) { [math]::Round(($times | Measure-Object -Average).Average, 1) } else { $null }) } } function ConvertTo-PrintingFinding { [CmdletBinding()] [OutputType([psobject])] param( [AllowNull()]$Data, [hashtable]$Parameters = @{} ) if (-not (Get-DataProperty $Data 'SpoolerFound')) { return New-UnavailableFinding -Category System -Check (Get-Text 'Check.Printing.Spooler') ` -Hint (Get-Text 'Hint.Printing.SpoolerNotFound') } if ((Get-DataProperty $Data 'SpoolerState') -ne 'Running') { # Nothing below means anything without the spooler: every printer reads as absent. return New-Finding -Category System -Check (Get-Text 'Check.Printing.Spooler') -Severity FAIL ` -Value ((Get-Text 'Value.Printing.SpoolerState') -f (Get-DataProperty $Data 'SpoolerState'), (Get-DataProperty $Data 'SpoolerMode')) ` -Hint (Get-Text 'Hint.Printing.SpoolerNotRunning') } New-Finding -Category System -Check (Get-Text 'Check.Printing.Spooler') -Severity OK -Value (Get-Text 'Value.Printing.Running') New-DefaultPrinterFinding -Data $Data -Parameters $Parameters New-OfflinePrinterFinding -Data $Data -Parameters $Parameters New-StuckPrintJobFinding -Data $Data -Parameters $Parameters New-PrintErrorFinding -Data $Data -Parameters $Parameters } function Test-PrinterOffline { <# .SYNOPSIS Whether a printer says it cannot print, by any of the three ways Windows says it. .DESCRIPTION PrinterStatus and ExtendedPrinterStatus 7 are Offline; ExtendedPrinterStatus 9 is Error. WorkOffline is the "use printer offline" switch a user or a driver flipped. #> [CmdletBinding()] [OutputType([bool])] param([Parameter(Mandatory)]$Printer) if (Get-DataProperty $Printer 'WorkOffline') { return $true } $status = ConvertTo-Number (Get-DataProperty $Printer 'PrinterStatus') $extended = ConvertTo-Number (Get-DataProperty $Printer 'ExtendedPrinterStatus') ($status -eq 7) -or ($extended -eq 7) -or ($extended -eq 9) } function New-DefaultPrinterFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warnMs = Get-Parameter $Parameters 'PrinterConnectWarnMs' 200 $failMs = Get-Parameter $Parameters 'PrinterConnectFailMs' ([double]::MaxValue) $check = Get-Text 'Check.Printing.DefaultPrinter' $default = @((Get-DataCollection $Data 'Printers') | Where-Object { $_.Default }) | Select-Object -First 1 if (-not $default) { return New-Finding -Category System -Check $check -Severity INFO -Value (Get-Text 'Value.Shared.None') ` -Hint (Get-Text 'Hint.Printing.NoDefaultPrinter') } $offline = 'OK' if (Test-PrinterOffline -Printer $default) { $offline = 'WARN' } $reach = Get-DataProperty $Data 'Reachability' $via = Get-DataProperty $reach 'Via' if (-not $reach -or $via -eq 'None') { # USB, WSD, a PDF writer: nothing to connect to, so the status is all there is. $value = (Get-Text 'Value.Printing.DefaultLocal') -f $default.Name, $default.PortName return New-Finding -Category System -Check $check -Severity $offline -Value $value ` -Hint (Get-Text 'Hint.Printing.DefaultOffline') } $target = '{0}:{1}' -f (Get-DataProperty $reach 'HostName'), (Get-DataProperty $reach 'Port') $successes = ConvertTo-Number (Get-DataProperty $reach 'Successes') $attempts = ConvertTo-Number (Get-DataProperty $reach 'Attempts') if (-not (Get-DataProperty $reach 'Resolved') -or -not $successes) { # The case this Check exists for: every document Office opens waits on this. return New-Finding -Category System -Check $check -Severity FAIL ` -Value ((Get-Text 'Value.Printing.DefaultUnreachable') -f $default.Name, $target) ` -Hint (Get-Text 'Hint.Printing.DefaultUnreachable') } $average = ConvertTo-Number (Get-DataProperty $reach 'AverageMs') $missed = 'OK' if ($successes -lt $attempts) { $missed = 'WARN' } $slow = 'OK' if ($null -ne $average) { $slow = Get-Severity $average $warnMs $failMs } New-Finding -Category System -Check $check -Severity (Get-WorstSeverity $offline $missed $slow) ` -Value ((Get-Text 'Value.Printing.DefaultReachable') -f $default.Name, $target, $average, $successes, $attempts) ` -Hint (Get-Text 'Hint.Printing.DefaultSlowOrOffline') } function New-OfflinePrinterFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'OfflinePrinterWarnAbove' 2 $fail = Get-Parameter $Parameters 'OfflinePrinterFailAbove' ([double]::MaxValue) # The default printer has its own Finding; counting it here too would say it twice. $offline = @((Get-DataCollection $Data 'Printers') | Where-Object { -not $_.Default -and (Test-PrinterOffline -Printer $_) }) $check = Get-Text 'Check.Printing.OfflinePrinters' if (-not $offline.Count) { return New-Finding -Category System -Check $check -Severity OK -Value (Get-Text 'Value.Shared.None') } $severity = Get-Severity $offline.Count $warn $fail if ($severity -eq 'OK') { $severity = 'INFO' } New-Finding -Category System -Check $check -Severity $severity ` -Value ((Get-Text 'Value.Printing.OfflinePrinters') -f $offline.Count, (($offline | ForEach-Object { $_.Name }) -join ', ')) ` -Hint (Get-Text 'Hint.Printing.OfflinePrinters') } function New-StuckPrintJobFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $minutes = Get-Parameter $Parameters 'PrintJobStuckMinutes' 10 $warn = Get-Parameter $Parameters 'StuckPrintJobWarnAbove' 0 $fail = Get-Parameter $Parameters 'StuckPrintJobFailAbove' 10 $stuck = @((Get-DataCollection $Data 'Jobs') | Where-Object { $age = ConvertTo-Number $_.AgeMinutes $null -ne $age -and $age -gt $minutes }) $check = Get-Text 'Check.Printing.StuckJobs' if (-not $stuck.Count) { return New-Finding -Category System -Check $check -Severity OK -Value (Get-Text 'Value.Shared.None') } $printers = ($stuck | ForEach-Object { $_.Printer } | Sort-Object -Unique) -join ', ' New-Finding -Category System -Check $check -Severity (Get-Severity $stuck.Count $warn $fail) ` -Value ((Get-Text 'Value.Printing.StuckJobs') -f $stuck.Count, $minutes, $printers) ` -Hint (Get-Text 'Hint.Printing.StuckJobs') } function New-PrintErrorFinding { [CmdletBinding()] param([AllowNull()]$Data, [hashtable]$Parameters) $warn = Get-Parameter $Parameters 'PrintErrorWarnAbove' 10 $fail = Get-Parameter $Parameters 'PrintErrorFailAbove' ([double]::MaxValue) $check = Get-Text 'Check.Printing.Errors' if (Get-DataProperty $Data 'LogUnreadable') { return New-UnavailableFinding -Category System -Check $check -Hint (Get-Text 'Hint.Printing.LogUnreadable') } $errors = Get-DataCollection $Data 'Errors' $days = Get-DataProperty $Data 'Days' if (-not $errors.Count) { return New-Finding -Category System -Check $check -Severity OK -Value ((Get-Text 'Value.Printing.NoErrors') -f $days) } # The most frequent event id is the lead: 808 is a driver that will not load into the # spooler, 372 a document that failed to print, 315-367 a printer that would not start. $common = $errors | Group-Object Id | Sort-Object Count -Descending | Select-Object -First 3 | ForEach-Object { 'ID {0} {1}x' -f $_.Name, $_.Count } New-Finding -Category System -Check $check -Severity (Get-Severity $errors.Count $warn $fail) ` -Value ((Get-Text 'Value.Printing.Errors') -f $errors.Count, $days, ($common -join ', ')) ` -Hint (Get-Text 'Hint.Printing.Errors') } function ConvertTo-PrintingSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) New-Section -Title (Get-Text 'Title.Printing.Printers') -Row @( (Get-DataCollection $Data 'Printers') | Select-Object Name, Default, Network, WorkOffline, PrinterStatus, ExtendedPrinterStatus, PortName, ServerName, DriverName ) New-Section -Title (Get-Text 'Title.Printing.Jobs') -Row (Get-DataCollection $Data 'Jobs') } function ConvertTo-PrintingEvent { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) (Get-DataCollection $Data 'Errors') } |