ReportRunner.psm1
<# #> ######## # Global settings $ErrorActionPreference = "Stop" $InformationPreference = "Continue" Set-StrictMode -Version 2 ######## # Add types Add-Type -AssemblyName 'System.Web' # List of library items that can be referenced in Add-ReportRunnerSection $Script:Definitions = New-Object 'System.Collections.Generic.Dictionary[string, ScriptBlock]' Class ReportRunnerSection { [string]$Name [string]$Description [PSObject[]]$Items [HashTable]$Data ReportRunnerSection([string]$name, [string]$description, [PSObject[]]$items, [HashTable]$data) { $this.Name = $name $this.Description = $description $this.Items = $items $this.Data = $data } } class ReportRunnerSectionContent { [string]$Name [string]$Description [System.Collections.Generic.LinkedList[PSObject]]$Content ReportRunnerSectionContent([string]$Name, [string]$Description) { $this.Name = $name $this.Description = $description $this.Content = New-Object 'System.Collections.Generic.LinkedList[PSObject]' } } Class ReportRunnerFormatTable { $Content } Class ReportRunnerContext { [System.Collections.Generic.List[ReportRunnerSection]]$Entries ReportRunnerContext() { $this.Entries = New-Object 'System.Collections.Generic.LinkedList[ReportRunnerSection]' } } enum ReportRunnerStatus { None = 0 Info Warning Error InternalError } <# #> Class ReportRunnerNotice { [ReportRunnerStatus]$Status [string]$Description ReportRunnerNotice([ReportRunnerStatus]$status, [string]$description) { $this.Status = $status $this.Description = $description } [string] ToString() { return ("{0}: {1}" -f $this.Status.ToString().ToUpper(), $this.Description) } } <# #> Function New-ReportRunnerNotice { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Description, [Parameter(mandatory=$false)] [ValidateNotNull()] [ReportRunnerStatus]$Status = [ReportRunnerStatus]::None ) process { $notice = New-Object ReportRunnerNotice -ArgumentList $Status, $Description $notice } } <# #> Function New-ReportRunnerFormatTable { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param( [Parameter(mandatory=$true)] [ValidateNotNull()] $Content ) process { $format = New-Object 'ReportRunnerFormatTable' $format.Content = $Content $format } } <# #> Function New-ReportRunnerContext { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] [OutputType('ReportRunnerContext')] param( ) process { $obj = New-Object ReportRunnerContext $obj } } <# #> Function Add-ReportRunnerDefinition { [CmdletBinding()] param( [Parameter(Mandatory=$true, HelpMessage = "Must be in module.group.id format")] [ValidateNotNullOrEmpty()] [ValidatePattern("^[a-zA-Z_-]*\.[a-zA-Z_-]*\.[a-zA-Z_-]*$")] [string]$Name, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$Script ) process { $script:Definitions[$Name] = $Script } } <# #> Function Add-ReportRunnerSection { [CmdletBinding()] param( [Parameter(Mandatory=$true,ValueFromPipeline)] [ValidateNotNull()] [ReportRunnerContext]$Context, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory=$false)] [ValidateNotNull()] [AllowEmptyString()] [string]$Description = "", [Parameter(Mandatory=$false)] [ValidateNotNull()] [PSObject[]]$Items = [PSObject[]]@(), [Parameter(Mandatory=$false)] [AllowNull()] [HashTable]$Data = $null ) process { # Add the script to the list of scripts to process $entry = New-Object 'ReportRunnerSection' -ArgumentList $Name, $Description, $Items, $Data $Context.Entries.Add($entry) | Out-Null } } <# #> Function Invoke-ReportRunnerContext { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNull()] [ReportRunnerContext]$Context ) process { $Context.Entries | ForEach-Object { $entry = $_ # Create a list of the scripts to run for this context section $scripts = New-Object 'System.Collections.Generic.LinkedList[ScriptBlock]' # Add any scripts defined specifically for this context section $entry.Items | ForEach-Object { $item = $_ switch ($item.GetType().FullName) { "System.String" { $script:Definitions.Keys | Where-Object { $_ -match [string]$item } | ForEach-Object { $scripts.Add($script:Definitions[$_]) } break } "System.Management.Automation.ScriptBlock" { $scripts.Add($item) break } default { Write-Error "Unknown item type: $_" } } } # Output a section format object $content = New-Object 'ReportRunnerSectionContent' -ArgumentList $entry.Name, $entry.Description $scripts | ForEach-Object { $script = $_ Invoke-Command -NoNewScope { # Run the script block try { ForEach-Object -InputObject $entry.Data -Process $script } catch { New-ReportRunnerNotice -Status InternalError -Description "Error running script: $_" } } } *>&1 | ForEach-Object { $content.Content.Add($_) | Out-Null } $content } } } <# #> Function Format-ReportRunnerContentAsHtml { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] [OutputType([System.String])] param( [Parameter(Mandatory=$true,ValueFromPipeline)] [ValidateNotNull()] [ReportRunnerSectionContent]$Section, [Parameter(Mandatory=$false)] [AllowEmptyString()] [ValidateNotNull()] [string]$Title = "", [Parameter(Mandatory=$false)] [bool]$DecodeHtml = $true ) begin { # Collection of all notices across all sections $allNotices = [ordered]@{} $allSectionContent = New-Object 'System.Collections.ArrayList' # Html preamble "<!DOCTYPE html PUBLIC `"-//W3C//DTD XHTML 1.0 Strict//EN`" `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd`">" "<html xmlns=`"http://www.w3.org/1999/xhtml`">" "<head>" "<title>$Title</title>" "<style>" "table {" " font-family: Arial, Helvetica, sans-serif;" " border-collapse: collapse;" " width: 100%;" "}" "td, th {" " border: 1px solid #ddd;" " padding: 6px;" "}" "div.section tr:nth-child(even){background-color: #f2f2f2;}" "div.section tr:hover {background-color: #ddd;}" ".warningCell {background-color: #ffeb9c;}" ".errorCell {background-color: #ffc7ce;}" ".internalErrorCell {background-color: #ffc7ce;}" "div.section th {" " padding-top: 12px;" " padding-bottom: 12px;" " text-align: left;" " background-color: #04AA6D;" " color: white;" "}" "</style>" "</head><body>" "<h2>$Title</h2>" } process { # Generate string content for this section $sectionContent = & { $notices = New-Object 'System.Collections.Generic.LinkedList[ReportRunnerNotice]' # Display section heading ("<h3>Section: {0}</h3>" -f $Section.Name) ("<i>{0}</i><br><p />" -f $Section.Description) "<table><tr><td>" $output = $Section.Content | ForEach-Object { # Default message to pass on in pipeline $msg = $_ # Check if it is a string or status object if ([ReportRunnerNotice].IsAssignableFrom($msg.GetType())) { [ReportRunnerNotice]$notice = $_ $notices.Add($notice) | Out-Null if ($allNotices.Keys -notcontains $Section.Name) { $allNotices[$Section.Name] = New-Object 'System.Collections.Generic.LinkedList[ReportRunnerNotice]' } $allNotices[$Section.Name].Add($notice) | Out-Null # Alter message to notice string representation $msg = $notice.ToString() } if ([System.Management.Automation.InformationRecord].IsAssignableFrom($_.GetType())) { $msg = ("INFO: {0}" -f $_.ToString()) } elseif ([System.Management.Automation.VerboseRecord].IsAssignableFrom($_.GetType())) { $msg = ("VERBOSE: {0}" -f $_.ToString()) } elseif ([System.Management.Automation.ErrorRecord].IsAssignableFrom($_.GetType())) { $msg = ("ERROR: {0}" -f $_.ToString()) } elseif ([System.Management.Automation.DebugRecord].IsAssignableFrom($_.GetType())) { $msg = ("DEBUG: {0}" -f $_.ToString()) } elseif ([System.Management.Automation.WarningRecord].IsAssignableFrom($_.GetType())) { $msg = ("WARNING: {0}" -f $_.ToString()) } if ([ReportRunnerFormatTable].IsAssignableFrom($msg.GetType())) { $msg = $msg.Content | ConvertTo-Html -As Table -Fragment } if ([string].IsAssignableFrom($msg.GetType())) { $msg += "<br>" if ($DecodeHtml) { $msg = [System.Web.HttpUtility]::HtmlDecode($msg) } } # Pass message on in the pipeline $msg } # Display notices for this section if (($notices | Measure-Object).Count -gt 0) { "<h4>Notices</h4><div class=`"section`">" $notices | ConvertTo-Html -As Table -Fragment | Update-ReportRunnerNoticeCellClass "<br></div>" } # Display output "<h4>Content</h4><div class=`"section`">" $output | Out-String "<br></div>" "<p />" "</td></tr></table>" } | Out-String $allSectionContent.Add($sectionContent) | Out-Null } end { # Display all notices here "<h3>All Notices</h3>" "<i>Notices generated by any section</i><br><p /><div class=`"section`">" $allNotices.Keys | ForEach-Object { $key = $_ $allNotices[$key] | ForEach-Object { $notice = $_ [PSCustomObject]@{ Section = $key Status = $notice.Status Description = $notice.Description } } } | ConvertTo-Html -As Table -Fragment | Update-ReportRunnerNoticeCellClass "<p /></div>" # Display all section content $allSectionContent | ForEach-Object { $_ } # Wrap up HTML "</body></html>" } } Function Update-ReportRunnerNoticeCellClass { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] [OutputType('System.String')] param( [Parameter(Mandatory=$true,ValueFromPipeline)] [AllowNull()] [string]$Content ) process { $val = $Content $val = $val.Replace("<td>Warning</td>", "<td class=`"warningCell`">Warning</td>") $val = $val.Replace("<td>Error</td>", "<td class=`"errorCell`">Error</td>") $val = $val.Replace("<td>InternalError</td>", "<td class=`"internalErrorCell`">InternalError</td>") $val } } |