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+$', '') # Not what the job is called: that is what a User printed (ADR-0006). 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 SilentlyContinue | ForEach-Object { ConvertTo-PrintErrorRow -LogEntry $_ }) } 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 # The machine's own addresses with their prefix lengths, for the Zone of what the # default printer is reached through. See Get-OwnPrefix. Prefixes = @(Get-OwnPrefix) } } 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 $address = $hostName if ($hostName -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { $resolution = Resolve-HostAddress -HostName $hostName $resolved = [bool]$resolution.Resolved $address = "$($resolution.Address)" } $times = @() $route = $null if ($resolved) { # The adapter the way leaves through, which the Judge decides the Zone by. $route = Get-AddressRoute -Address $address $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 }) Address = $address Route = $route } } 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-ServiceStateText -State (Get-DataProperty $Data 'SpoolerState')), (ConvertTo-ServiceStartModeText -StartMode (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') $default = New-DefaultPrinterFinding -Data $Data -Parameters $Parameters $default New-OfflinePrinterFinding -Data $Data -Parameters $Parameters New-StuckPrintJobFinding -Data $Data -Parameters $Parameters New-PrintErrorFinding -Data $Data -Parameters $Parameters # And the time to the print Servers, for the Report's picture of the network. ADR-0009. ConvertTo-PrintingMeasurement -Data $Data -Parameters $Parameters -Finding $default } function ConvertTo-PrintingMeasurement { <# .SYNOPSIS The time to each print Server, as the Finding about the default printer measured it. Pure. .DESCRIPTION Only what the default printer is reached through is connected to: its print Server, or the printer itself where it has its own TCP/IP port - that is what this machine prints to, and is drawn as its print Server. Every other print Server the printers name is said as not measured, with no Finding, so that the picture shows it and does not show it fast. .PARAMETER Finding The Finding about the default printer. #> [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data, [hashtable]$Parameters = @{}, [AllowNull()]$Finding) $reach = Get-DataProperty $Data 'Reachability' $measured = '' $hostName = "$(Get-DataProperty $reach 'HostName')" if ($hostName -and "$(Get-DataProperty $reach 'Via')" -ne 'None') { $measured = $hostName $resolved = [bool](Get-DataProperty $reach 'Resolved') $address = '' $zone = '' if ($resolved) { $address = "$(Get-DataProperty $reach 'Address')" $vpnPattern = Get-Parameter $Parameters 'VpnAdapterPattern' $script:MeasurementVpnAdapterPattern $zone = Get-GatheredAddressZone -Data $Data -Address $address -Route (Get-DataProperty $reach 'Route') -VpnPattern $vpnPattern } $common = @{ Subject = $hostName; Address = $address; Zone = $zone; Role = 'PrintServer' Port = (Get-DataProperty $reach 'Port'); Finding = $Finding } if (-not $resolved) { New-Measurement @common -Metric TcpConnect -State Failed -ErrorText (Get-Text 'Measurement.Error.NotResolved') } else { ConvertTo-TcpConnectMeasurement -Common $common -Result $reach } } $seen = @{} if ($measured) { $seen[$measured.ToLowerInvariant()] = $true } foreach ($printer in (Get-DataCollection $Data 'Printers')) { $server = "$(Get-DataProperty $printer 'ServerName')".TrimStart('\').Trim() if (-not $server -or $seen.ContainsKey($server.ToLowerInvariant())) { continue } $seen[$server.ToLowerInvariant()] = $true New-Measurement -Subject $server -Role PrintServer -Metric TcpConnect -Port $script:PrintingServerPort -State NotMeasured } } 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 if ($offline -ne 'OK') { $value = '{0} | {1}' -f $value, (Get-Text 'Value.Printing.Reason.Offline') } 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 } # What is wrong with it, said. The Value used to read "... Verbindung Mittel 1 ms (3/3 # ok)" under a WARN, and the Hint "slow, unreliable or offline": a Technician could not # tell which, and here it was none of the first two. $reasons = @() $hints = @() if ($offline -ne 'OK') { $reasons += Get-Text 'Value.Printing.Reason.Offline' $hints += Get-Text 'Hint.Printing.DefaultOffline' } if ($missed -ne 'OK') { $reasons += Get-Text 'Value.Printing.Reason.Missed' ($attempts - $successes) $attempts } if ($slow -ne 'OK') { $reasons += Get-Text 'Value.Printing.Reason.Slow' $average } if ($missed -ne 'OK' -or $slow -ne 'OK') { $hints += Get-Text 'Hint.Printing.DefaultSlow' } $value = (Get-Text 'Value.Printing.DefaultReachable') -f $default.Name, $target, $average, $successes, $attempts if ($reasons.Count) { $value = '{0} | {1}' -f $value, ($reasons -join ' | ') } New-Finding -Category System -Check $check -Severity (Get-WorstSeverity $offline $missed $slow) ` -Value $value -Hint ($hints -join ' | ') } 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') } # Counted as devices. One printer is often several queues on its print server - one # for each tray, one for duplex - whose ports are the printer's address with _1, _2 # behind it, and four queues of one printer are one printer that reports offline. $devices = @($offline | Group-Object { '{0}|{1}' -f "$($_.ServerName)".ToLowerInvariant(), ("$($_.PortName)" -replace '_\d{1,2}$', '').ToLowerInvariant() } | ForEach-Object { $name = @($_.Group | ForEach-Object { "$($_.Name)" } | Sort-Object { $_.Length }, { $_ })[0] [pscustomobject]@{ Name = $name; Queues = $_.Count; OnServer = [bool]"$($_.Group[0].ServerName)" } } | Sort-Object Name) $severity = Get-Severity $devices.Count $warn $fail if ($severity -eq 'OK') { $severity = 'INFO' } $named = @($devices | ForEach-Object { if ($_.Queues -gt 1) { (Get-Text 'Value.Printing.OfflineQueues') -f $_.Name, $_.Queues } else { $_.Name } }) # "Offline" is what Windows was told, and by whom differs: a print server says it of # a printer that does not answer its SNMP query, whether or not the printer prints. # That a printer is offline does not make it one nobody uses. $meaning = @() if (@($devices | Where-Object { $_.OnServer }).Count) { $meaning += Get-Text 'Meaning.Printing.OfflineOnServer' } if (@($devices | Where-Object { -not $_.OnServer }).Count) { $meaning += Get-Text 'Meaning.Printing.OfflineLocal' } New-Finding -Category System -Check $check -Severity $severity ` -Value ((Get-Text 'Value.Printing.OfflinePrinters') -f $devices.Count, ($named -join ', ')) ` -Meaning ($meaning -join ' | ') -Hint (Get-Text 'Hint.Printing.OfflinePrinters') ` -Signal 'printer-offline' ` -Reference @((New-Reference -Signal 'print-job-stuck'), (New-Reference -Section 'Title.Printing.Printers')) } function Format-PrintJobAge { <# .SYNOPSIS How long a job has been lying in a queue, in the unit that says it best. Pure. #> [CmdletBinding()] [OutputType([string])] param([double]$Minutes) if ($Minutes -ge 2880) { return (Get-Text 'Value.Printing.Age.Days') -f [math]::Floor($Minutes / 1440) } if ($Minutes -ge 120) { return (Get-Text 'Value.Printing.Age.Hours') -f [math]::Floor($Minutes / 60) } (Get-Text 'Value.Printing.Age.Minutes') -f [math]::Floor($Minutes) } 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 ', ' $oldest = ($stuck | ForEach-Object { ConvertTo-Number $_.AgeMinutes } | Measure-Object -Maximum).Maximum # A job in a print server's queue is not this machine's to restart a service over. $hint = Get-Text 'Hint.Printing.StuckJobs' if (@($stuck | Where-Object { "$($_.Printer)".StartsWith('\\') }).Count) { $hint = Get-Text 'Hint.Printing.StuckJobsOnServer' } New-Finding -Category System -Check $check -Severity (Get-Severity $stuck.Count $warn $fail) ` -Value ((Get-Text 'Value.Printing.StuckJobs') -f $stuck.Count, $minutes, $printers, (Format-PrintJobAge -Minutes $oldest)) ` -Meaning (Get-Text 'Meaning.Printing.StuckJobs') -Hint $hint ` -Signal 'print-job-stuck' ` -Reference @((New-Reference -Signal 'printer-offline'), (New-Reference -Section 'Title.Printing.Jobs')) } 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, and is said with what it means. $common = $errors | Group-Object Id | Sort-Object Count -Descending | Select-Object -First 3 | ForEach-Object { $id = Format-CodeWithMeaning -Code ('ID {0}' -f $_.Name) -Meaning (Get-PrintErrorIdText -Id $_.Name) (Get-Text 'Value.Printing.ErrorCount') -f $id, $_.Count } # Which printers, the one with the most errors first: that is whose driver to look at. $printers = @($errors | Group-Object { "$(Get-DataProperty $_ 'Printer')" } | Sort-Object @{ Expression = 'Count'; Descending = $true }, Name | Select-Object -First 5 | ForEach-Object { $name = $_.Name if (-not $name) { $name = Get-Text 'Value.Printing.ErrorNoPrinter' } '{0} {1}x' -f $name, $_.Count }) $value = (Get-Text 'Value.Printing.Errors') -f $errors.Count, $days, ($common -join ', ') if ($printers.Count) { $value = '{0} | {1}' -f $value, ((Get-Text 'Value.Printing.ErrorPrinters') -f ($printers -join '; ')) } New-Finding -Category System -Check $check -Severity (Get-Severity $errors.Count $warn $fail) ` -Value $value -Hint (Get-Text 'Hint.Printing.Errors') } function Get-PrintErrorIdText { <# .SYNOPSIS What an error event of the print service means. Nothing for one not known here, which is then shown as the number it is. Pure. .DESCRIPTION Not a table of Microsoft's: read off the messages the Microsoft-Windows-PrintService provider carries for its events ((Get-WinEvent -ListProvider ...).Events), each error by its own message. The numbers between them are events that are no error - a driver added, a printer published - and have no meaning here, as they are not among what is counted. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$Id) $number = ConvertTo-Number $Id if ($null -eq $number) { return '' } switch ([int]$number) { 808 { return (Get-Text 'Value.Printing.ErrorId.DriverLoad') } 372 { return (Get-Text 'Value.Printing.ErrorId.DocumentFailed') } 314 { return (Get-Text 'Value.Printing.ErrorId.Timeout') } 315 { return (Get-Text 'Value.Printing.ErrorId.ShareFailed') } 371 { return (Get-Text 'Value.Printing.ErrorId.UnshareFailed') } 318 { return (Get-Text 'Value.Printing.ErrorId.SettingsReset') } 319 { return (Get-Text 'Value.Printing.ErrorId.DriverMissing') } 320 { return (Get-Text 'Value.Printing.ErrorId.PortMissing') } { $_ -in 322, 323, 325, 326, 327, 328, 329, 331, 333, 335, 337 } { return (Get-Text 'Value.Printing.ErrorId.DirectoryPublish') } { $_ -in 348, 351 } { return (Get-Text 'Value.Printing.ErrorId.DriverIncompatible') } 350 { return (Get-Text 'Value.Printing.ErrorId.SpoolFileCorrupt') } 353 { return (Get-Text 'Value.Printing.ErrorId.NoRights') } 354 { return (Get-Text 'Value.Printing.ErrorId.InitFailed') } 360 { return (Get-Text 'Value.Printing.ErrorId.ColorProfile') } 361 { return (Get-Text 'Value.Printing.ErrorId.PortInit') } 362 { return (Get-Text 'Value.Printing.ErrorId.OwnNameNotResolved') } 363 { return (Get-Text 'Value.Printing.ErrorId.SpoolerStart') } { $_ -in 364, 365, 367 } { return (Get-Text 'Value.Printing.ErrorId.PrintProcessor') } 366 { return (Get-Text 'Value.Printing.ErrorId.SecurityDescriptor') } { $_ -in 368, 369, 370 } { return (Get-Text 'Value.Printing.ErrorId.DriverPackage') } 373 { return (Get-Text 'Value.Printing.ErrorId.GdiObjects') } } '' } # Where Windows puts the printer in the payload of a print error, by event id, counted # from 0. Read off the texts of the provider's own events (Get-WinEvent -ListProvider # Microsoft-Windows-PrintService, German Windows 11 26300, 2026-10-11): "Das Dokument %1 # im Besitz von %2 konnte nicht auf dem Drucker %3 gedruckt werden" is 372. $script:PrintErrorPrinterIndex = @{ 372 = 2; 314 = 3 318 = 0; 319 = 0; 320 = 0; 359 = 0; 360 = 0; 361 = 0; 367 = 0 315 = 1; 371 = 1 } # The events whose text names the document and whose it was. Their text is not kept. $script:PrintErrorNamesDocument = @(314, 350, 372) function ConvertTo-PrintErrorRow { <# .SYNOPSIS One print error, reduced to when, which event, which printer, and what Windows said - unless what it said names a document. Pure. .DESCRIPTION What was printed and by whom is what a User did, and nothing a Report holds (ADR-0006). Windows writes both into the text of a failed print; that text is dropped, and the printer is taken from the payload by its place. #> [CmdletBinding()] [OutputType([psobject])] param([Parameter(Mandatory)]$LogEntry) $id = [int]$LogEntry.Id $printer = '' if ($script:PrintErrorPrinterIndex.ContainsKey($id)) { $printer = "$(Get-EventPayload -LogEntry $LogEntry -Index $script:PrintErrorPrinterIndex[$id])".Trim() } $row = ConvertTo-EventRow -LogEntry $LogEntry -Tag 'Print error' if ($script:PrintErrorNamesDocument -contains $id) { $row.Message = '' } $row | Add-Member -NotePropertyName 'Printer' -NotePropertyValue $printer -PassThru } function ConvertTo-PrintingSection { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) New-Section -Title (Get-Text 'Title.Printing.Printers') -Help (Get-Text 'Help.Printing.Printers') -Row @( (Get-DataCollection $Data 'Printers') | ForEach-Object { $row = [ordered]@{} $row[(Get-Text 'Column.Printing.Name')] = $_.Name $row[(Get-Text 'Column.Printing.Default')] = Get-TriStateText ([bool]$_.Default) $row[(Get-Text 'Column.Printing.Network')] = Get-TriStateText ([bool]$_.Network) $row[(Get-Text 'Column.Printing.WorkOffline')] = Get-TriStateText ([bool]$_.WorkOffline) # What the two numbers mean, and the numbers: a code is never shown bare. $row[(Get-Text 'Column.Printing.Status')] = Get-PrinterStatusText -PrinterStatus $_.PrinterStatus -ExtendedPrinterStatus $_.ExtendedPrinterStatus $row[(Get-Text 'Column.Printing.Port')] = $_.PortName $row[(Get-Text 'Column.Printing.Server')] = $_.ServerName $row[(Get-Text 'Column.Printing.Driver')] = $_.DriverName [pscustomobject]$row } ) New-Section -Title (Get-Text 'Title.Printing.Jobs') -Help (Get-Text 'Help.Printing.Jobs') -Row @( (Get-DataCollection $Data 'Jobs') | ForEach-Object { $age = ConvertTo-Number (Get-DataProperty $_ 'AgeMinutes') $row = [ordered]@{} $row[(Get-Text 'Column.Printing.Name')] = Get-DataProperty $_ 'Printer' # In Windows' own words and language: the spooler hands it over as text. $row[(Get-Text 'Column.Printing.Job.Status')] = Get-DataProperty $_ 'JobStatus' $row[(Get-Text 'Column.Printing.Job.Age')] = $(if ($null -ne $age) { Format-PrintJobAge -Minutes $age } else { '' }) [pscustomobject]$row } ) # The errors themselves, so that the Finding can point here and not at a file. $errors = @((Get-DataCollection $Data 'Errors') | Sort-Object { ConvertTo-DataTime (Get-DataProperty $_ 'Time') } -Descending | Select-Object -First 200) if ($errors.Count) { New-Section -Title (Get-Text 'Title.Printing.Errors') -Help (Get-Text 'Help.Printing.Errors') -Row @( $errors | ForEach-Object { $id = Get-DataProperty $_ 'Id' $row = [ordered]@{} $row[(Get-Text 'Column.Printing.Error.Time')] = Get-DataProperty $_ 'Time' # The number with what it means: a code is never shown bare. $row[(Get-Text 'Column.Printing.Error.Event')] = Format-CodeWithMeaning -Code ('ID {0}' -f $id) -Meaning (Get-PrintErrorIdText -Id $id) $row[(Get-Text 'Column.Printing.Name')] = Get-DataProperty $_ 'Printer' $row[(Get-Text 'Column.Printing.Error.Message')] = Get-DataProperty $_ 'Message' [pscustomobject]$row } ) } } function Get-PrinterStatusText { <# .SYNOPSIS What Windows reports of a printer, in a word, with the two numbers behind it. Pure. .DESCRIPTION Win32_Printer has two status properties, and the extended one says more: 3 idle, 4 printing, 7 offline, 8 paused, 9 error and so on, as Microsoft documents them for that class. Where the extended one is "unknown" or absent, the plain one (3 idle, 4 printing, 7 offline) is what there is. #> [CmdletBinding()] [OutputType([string])] param([AllowNull()]$PrinterStatus, [AllowNull()]$ExtendedPrinterStatus) $plain = ConvertTo-Number $PrinterStatus $extended = ConvertTo-Number $ExtendedPrinterStatus $read = $extended if ($null -eq $read -or $read -in 1, 2) { if ($null -ne $plain -and $plain -notin 1, 2) { $read = $plain } } $text = switch ($read) { 2 { Get-Text 'Value.Printing.Status.Unknown' } 3 { Get-Text 'Value.Printing.Status.Idle' } 4 { Get-Text 'Value.Printing.Status.Printing' } 5 { Get-Text 'Value.Printing.Status.WarmingUp' } 6 { Get-Text 'Value.Printing.Status.Stopped' } 7 { Get-Text 'Value.Printing.Status.Offline' } 8 { Get-Text 'Value.Printing.Status.Paused' } 9 { Get-Text 'Value.Printing.Status.Error' } 10 { Get-Text 'Value.Printing.Status.Busy' } 11 { Get-Text 'Value.Printing.Status.NotAvailable' } 12 { Get-Text 'Value.Printing.Status.Waiting' } 13 { Get-Text 'Value.Printing.Status.Processing' } 14 { Get-Text 'Value.Printing.Status.Initializing' } 15 { Get-Text 'Value.Printing.Status.PowerSave' } 16 { Get-Text 'Value.Printing.Status.PendingDeletion' } 17 { Get-Text 'Value.Printing.Status.IoActive' } 18 { Get-Text 'Value.Printing.Status.ManualFeed' } default { Get-Text 'Value.Printing.Status.Other' } } (Get-Text 'Value.Printing.Status') -f $text, ('{0}/{1}' -f $PrinterStatus, $ExtendedPrinterStatus) } function ConvertTo-PrintingEvent { [CmdletBinding()] [OutputType([psobject])] param([AllowNull()]$Data) (Get-DataCollection $Data 'Errors') } |