systemchecks.psm1
|
#Requires -Version 5 <# .Synopsis Count the number of files in a directory. .Description Returns the count of files (not sub-directories) in the specified path. Optionally appends a date-based sub-folder to the base path using the AppendLeaf and LeafFormat parameters — handy for checking whether today's or yesterday's output files were created by a batch process. .Parameter FilePath Base directory path to check. .Parameter SystemName Friendly name for the system this check belongs to (used in reporting). .Parameter SystemDescription Short description of the system (used in reporting). .Parameter AppendLeaf When set, a date sub-folder is appended to FilePath. Accepted values: 'Today' (current date) or 'Yesterday' (previous day). .Parameter LeafFormat The date format string passed to Get-Date when building the sub-folder name, e.g. 'yyyyMMdd'. .Example Get-FileCount -FilePath "c:\my\folder" #> function Get-FileCount { [CmdletBinding()] param ( [string]$FilePath, [string]$SystemName, [string]$SystemDescription, [string]$AppendLeaf, [string]$LeafFormat ) $HealthCheckType = 'FileCount' try { if (Test-Path -Path $FilePath) { $FolderName = Split-Path -Path $FilePath -Leaf if (-not [string]::IsNullOrEmpty($AppendLeaf)) { switch ($AppendLeaf) { 'Today' { $ChildPath = Get-Date -Format $LeafFormat $CheckPath = Join-Path -Path $FilePath -ChildPath $ChildPath $FolderName += "\$ChildPath." } 'Yesterday' { $ChildPath = Get-Date -Date ((Get-Date).AddDays(-1)) -Format $LeafFormat $CheckPath = Join-Path -Path $FilePath -ChildPath $ChildPath $FolderName += "\$ChildPath." } } Write-Verbose "AppendLeaf: $AppendLeaf" Write-Verbose "CheckPath: $CheckPath" Write-Verbose "FolderName: $FolderName" } else { $CheckPath = $FilePath Write-Verbose "Skipping - AppendLeaf" } if (Test-Path -Path $CheckPath) { $FileCount = Get-ChildItem -Path $CheckPath -File | Measure-Object | Select-Object -ExpandProperty Count $Comment = $CheckPath Write-Verbose "Good test path - value: $CheckPath" } else{ $Exception = $Error[0].Exception.Message if ([string]::IsNullOrEmpty($Exception)) { $Exception = "Path not found: $CheckPath" } Write-Verbose $Exception $FileCount = 0 $Comment = $Exception } Write-Verbose "Comment: $Comment" return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $FolderName Type = $HealthCheckType Status = $FileCount LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $Comment ComputerName = $ENV:COMPUTERNAME } } else { $Exception = $Error[0].Exception.Message if ([string]::IsNullOrEmpty($Exception)) { $Exception = "Path not found: $FilePath" } Write-Verbose $Exception return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $FilePath Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $Exception ComputerName = $ENV:COMPUTERNAME } } } catch { $Exception = $Error[0].Exception.Message if ([string]::IsNullOrEmpty($Exception)) { $Exception = "Path not found: $FilePath" } Write-Verbose $Exception return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $FilePath Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $Exception ComputerName = $ENV:COMPUTERNAME } } } #Requires -Version 5 <# .Synopsis Run health checks against one or more systems defined in JSON config files. .Description Reads one or more JSON configuration files and runs the appropriate health checks (processes, services, files, shares, URIs, scheduled tasks, file counts) for each system defined. Results are collected into a flat list and written to an output JSON file under .\output_files\. .Parameter ConfigFileName One or more FileInfo or path objects pointing to the JSON configuration files to process. .Example Get-SystemHealth -ConfigFileName ".\config_files\system1.json",".\config_files\system2.json" #> function Get-SystemHealth { [CmdletBinding()] param ( [System.Object[]]$ConfigFileName ) $ConfigFileName | ForEach-Object { $ConfigFile = $_ $SystemHealthData = @() $file = Get-Content -Path $ConfigFile.FullName | ConvertFrom-Json $SystemName = $file.systemName $SystemDescription = $file.description $file.Processes | ForEach-Object { $procSplat = @{ ProcessName = $_.name SystemName = $SystemName SystemDescription = $SystemDescription } $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ProcessHealth @procSplat)) } $file.Services | ForEach-Object { $serviceSplat = @{ ServiceName = $_.name SystemName = $SystemName SystemDescription = $SystemDescription } $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ServiceHealth @serviceSplat)) } $file.FilesExist | ForEach-Object { $checkfileSplat = @{ FilePath = $_.FilePath SystemName = $SystemName SystemDescription = $SystemDescription } $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-FileExists @checkfileSplat)) } $file.SharesExist | ForEach-Object { $checkshareSplat = @{ SharePath = $_.SharePath SystemName = $SystemName SystemDescription = $SystemDescription } $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ShareExists @checkshareSplat)) } $File.URIs | ForEach-Object { $checkURISplat = @{ URI = $_.URI SystemName = $SystemName SystemDescription = $SystemDescription UseBasicParsing = $_.useBasicParsing UseDefaultCredentials = $_.useDefaultCredentials } $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-URIHealth @checkURISplat)) } $file.ScheduledTasks | ForEach-Object { $schedtaskSplat = @{ TaskPath = $_.TaskPath SystemName = $SystemName SystemDescription = $SystemDescription } $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Test-ScheduledTask @schedtaskSplat)) } $file.FileCount | ForEach-Object { $filecountSplat = @{ FilePath = $_.FilePath SystemName = $SystemName SystemDescription = $SystemDescription AppendLeaf = $_.appendLeaf LeafFormat = $_.leafFormat } $SystemHealthData = [System.Collections.ArrayList]$SystemHealthData; $null = $SystemHealthData.Add((Get-FileCount @filecountSplat)) } $OutFileName = ".\output_files\healthcheck_$($ENV:COMPUTERNAME)_$($ConfigFile.Name)" $SystemHealthData | ConvertTo-Json | Out-File (New-Item -Path $OutFileName -Force) $SystemHealthData } } #Requires -Version 5 <# .Synopsis This function calls the error lookup tool to get detailed information about an error. .Description This function is intended to be used with Get-SystemHealth and relies on $ScriptDirectory for proper function. Requires the Microsoft Error Lookup Tool (err.exe), included in the project. For details on error lookup tool see article: https://www.microsoft.com/en-us/download/details.aspx?id=100432&msockid=2fd802363d216c82121f16d63c406d64 The tool can also be installed by using winget (winget install Microsoft.err). This function expects the tool is NOT installed and runs it from the project folder. NOTE: By default, this function checks errors against the winerror.h file. .Parameter ErrorCode The numeric error code to look up. Accepts decimal or hex integers, e.g. 5 or 0x80070005. .Example Get-Win32Error 0x80070005 # Access Denied error #> function Get-Win32Error { [CmdletBinding()] param( [Parameter(Mandatory)] [int]$ErrorCode ) # Path to the Error Lookup Tool executable $errExe = Join-Path -Path $ScriptDirectory -ChildPath "Includes\err.exe" Write-Verbose "Error Tool Path: {$errExe}" # Check if the executable exists if (!(Test-Path $errExe)) { Write-Error "Error Lookup Tool not found at $errExe" return } # Run the tool and capture output $output = & $errExe "/winerror.h" $ErrorCode # Parse the output and return the message # this is a rough parse if ($output -match "winerror.h") { $returnvalue = $output -join " " $returnvalue = $returnvalue -replace "#", '`r`n' return $returnvalue } else { return "Error code not found" } } #Requires -Version 5 <# .Synopsis Check whether a file or directory path exists. .Description Uses Test-Path to determine whether the supplied path is present on disk. Returns 'Exists' when the path is found, 'Not Found' when it is absent, or 'ERROR' if an unexpected exception occurs (e.g. access denied, invalid path). .Parameter FilePath Full path to the file or directory to check. .Parameter SystemName Friendly name for the system this check belongs to (used in reporting). .Parameter SystemDescription Short description of the system (used in reporting). .Example Test-FileExists -FilePath "c:\my\file" #> function Test-FileExists { [CmdletBinding()] param ( [string]$FilePath, [string]$SystemName, [string]$SystemDescription ) $HealthCheckType = 'FileExists' $filename = Split-Path -Path $FilePath -Leaf try { if (Test-Path -Path $FilePath) { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $filename Type = $HealthCheckType Status = 'Exists' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $FilePath ComputerName = $ENV:COMPUTERNAME } } else { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $filename Type = $HealthCheckType Status = 'Not Found' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $FilePath ComputerName = $ENV:COMPUTERNAME } } } catch { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $FilePath Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $Error[0].Exception.Message ComputerName = $ENV:COMPUTERNAME } } } #Requires -Version 5 <# .Synopsis Check if a process is running and responding. .Description Uses Get-Process to find the named process and checks the Responding flag. Returns 'Responding' if the process is found and not hung, or 'ERROR' if the process is not running or is unresponsive. .Parameter ProcessName The name of the process to check (without the .exe extension). .Parameter SystemName Friendly name for the system this check belongs to (used in reporting). .Parameter SystemDescription Short description of the system (used in reporting). .Example Test-ProcessHealth -ProcessName "explorer" #> function Test-ProcessHealth { [CmdletBinding()] param ( [string]$ProcessName, [string]$SystemName, [string]$SystemDescription ) $HealthCheckType = 'Process' try { $process = Get-Process -Name $ProcessName -ErrorAction Stop if ($process.Responding -eq $true) { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ProcessName Type = $HealthCheckType Status = 'Responding' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = "" ComputerName = $ENV:COMPUTERNAME } } else { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ProcessName Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = "Process not responding. Status: $($process.Responding)" ComputerName = $ENV:COMPUTERNAME } } } catch { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ProcessName Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $_.Exception.Message ComputerName = $ENV:COMPUTERNAME } } } #Requires -Version 5 <# .Synopsis Get the last-run status of a scheduled task. .Description Retrieves task run information using Get-ScheduledTaskInfo and checks LastTaskResult. A result of 0 means the task completed successfully. Any other code is looked up via Get-Win32Error so you get a human-readable description rather than a raw hex value. .Parameter TaskPath Full task path including folder, e.g. '\Tasks\Send Email'. .Parameter SystemName Friendly name for the system this check belongs to (used in reporting). .Parameter SystemDescription Short description of the system (used in reporting). .Example Test-ScheduledTask -TaskPath "\Tasks\Send Email" #> function Test-ScheduledTask { [CmdletBinding()] param ( [string]$TaskPath, [string]$SystemName, [string]$SystemDescription ) $HealthCheckType = 'ScheduledTask' $task = Split-Path -Path $TaskPath -Leaf $path = Split-Path -Path $TaskPath -Parent try { $taskdetail = Get-ScheduledTaskInfo -TaskName $task -TaskPath $path -ErrorAction Stop if ($taskdetail -and $taskdetail.LastTaskResult -eq '0') { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $TaskPath Type = $HealthCheckType Status = 'OK' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = "LastRunTime: $($taskdetail.LastRunTime) NextRunTime: $($taskdetail.NextRunTime) MissedRuns: $($taskdetail.NumberOfMissedRuns)" ComputerName = $ENV:COMPUTERNAME } } else { if (!$null -eq $taskdetail) { $errorResult = Get-Win32Error -ErrorCode $taskdetail.LastTaskResult $parsedErrorMessage = $errorResult -split '`r`n' Write-Verbose "Parsed Error: {$parsedErrorMessage}" if (-not [string]::IsNullOrEmpty($parsedErrorMessage)) { $errMessage = $parsedErrorMessage[2].Trim() $errCodeConverted = $parsedErrorMessage[1].Trim() -replace '\s{2,}', ', ' } else { $errMessage = "" $errCodeConverted = 0 } return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $TaskPath Type = $HealthCheckType Status = ("{0} - {1}" -f $taskdetail.LastTaskResult, $errCodeConverted) LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $errMessage ComputerName = $ENV:COMPUTERNAME } } else { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $TaskPath Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = "Task not found." ComputerName = $ENV:COMPUTERNAME } } } } catch { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $TaskPath Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = "Task not found." ComputerName = $ENV:COMPUTERNAME } } } #Requires -Version 5 <# .Synopsis Check the running status of a Windows service. .Description Retrieves the named service using Get-Service and checks whether it is in the Running state. Returns 'OK' if running, or 'ERROR' with the current status in the Comment field if stopped, paused, or not found. .Parameter ServiceName The short service name (not the display name) to check, e.g. 'w3svc'. .Parameter SystemName Friendly name for the system this check belongs to (used in reporting). .Parameter SystemDescription Short description of the system (used in reporting). .Example Test-ServiceHealth -ServiceName 'w3svc' #> function Test-ServiceHealth { [CmdletBinding()] param ( [string]$ServiceName, [string]$SystemName, [string]$SystemDescription ) $HealthCheckType = 'Service' try { $service = Get-Service -Name $ServiceName -ErrorAction Stop if ($service.Status -eq 'Running') { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ServiceName Type = $HealthCheckType Status = 'OK' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = "" ComputerName = $ENV:COMPUTERNAME } } else { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ServiceName Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $service.Status ComputerName = $ENV:COMPUTERNAME } } } catch { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ServiceName Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $_.Exception.Message ComputerName = $ENV:COMPUTERNAME } } } #Requires -Version 5 <# .Synopsis Check whether a network share or local path is accessible. .Description Uses Test-Path to verify access to a UNC share, drive letter, or local path. Handles UNC paths (\\server\share), drive letters (C:), and plain paths, and extracts a meaningful share name for reporting in each case. Returns 'Exists' when the path is reachable, 'Not Found' when it is not, or 'ERROR' if an exception is raised. .Parameter SharePath The path to test, e.g. '\\server\e$' or 'D:\Data'. .Parameter SystemName Friendly name for the system this check belongs to (used in reporting). .Parameter SystemDescription Short description of the system (used in reporting). .Example Test-ShareExists -SharePath "\\server\e$" #> function Test-ShareExists { [CmdletBinding()] param ( [string]$SharePath, [string]$SystemName, [string]$SystemDescription ) $HealthCheckType = 'ShareExists' # Extract share name - handle UNC paths, drive letters, and regular paths if ($SharePath -match '^\\\\[^\\]+\\([^\\]+)') { # UNC path like \\server\share $ShareName = $matches[1] } elseif ($SharePath -match '^([A-Z]:)') { # Drive letter like C: $ShareName = $matches[1] } else { # Fall back to GetFileName for other paths $ShareName = [System.IO.Path]::GetFileName($SharePath) if ([string]::IsNullOrEmpty($ShareName)) { $ShareName = $SharePath } } try { if (Test-Path -Path $SharePath) { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ShareName Type = $HealthCheckType Status = 'Exists' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $SharePath ComputerName = $ENV:COMPUTERNAME } } else { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ShareName Type = $HealthCheckType Status = 'Not Found' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $SharePath ComputerName = $ENV:COMPUTERNAME } } } catch { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $ShareName Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $Error[0].Exception.Message ComputerName = $ENV:COMPUTERNAME } } } #Requires -Version 5 <# .Synopsis Compare the current date/time on two remote systems. .Description Opens a temporary PSSession to each system, retrieves the current date/time, and calculates the difference as a TimeSpan. Useful for spotting NTP drift between servers that need to stay in sync (e.g. domain controllers or cluster nodes). If a session cannot be established, the affected system is reported as 'ERROR' and the difference is returned as 0. .Parameter System1Name Hostname or IP address of the first system. .Parameter System2Name Hostname or IP address of the second system. .Example Test-TimeSync -System1Name "server1" -System2Name "server2" -Verbose #> function Test-TimeSync { [CmdletBinding()] param ( [string]$System1Name, [string]$System2Name ) $HealthCheckType = 'TimeSync' $session1 = New-PSSession -ComputerName $System1Name -ErrorAction SilentlyContinue if ($session1) { Write-Verbose "Collecting current date/time from $System1Name" $timedate = Invoke-Command -Session $session1 -ScriptBlock { Get-Date } $System1DateTime = [PSCustomObject]@{ SystemName = $System1Name SystemDateTime = $timedate Status = 'Success' Comment = '' } } else { $System1DateTime = [PSCustomObject]@{ SystemName = $System1Name SystemDateTime = '' Status = 'ERROR' Comment = "Unable to establish a session with '$System1Name'" } } $session2 = New-PSSession -ComputerName $System2Name -ErrorAction SilentlyContinue if ($session2) { Write-Verbose "Collecting current date/time from $System2Name" $timedate = Invoke-Command -Session $session2 -ScriptBlock { Get-Date } $System2DateTime = [PSCustomObject]@{ SystemName = $System2Name SystemDateTime = $timedate Status = 'Success' Comment = '' } } else { $System2DateTime = [PSCustomObject]@{ SystemName = $System2Name SystemDateTime = '' Status = 'ERROR' Comment = "Unable to establish a session with '$System2Name'" } } if ($System1DateTime.Status -eq 'ERROR' -or $System2DateTime.Status -eq 'ERROR') { $DateTimeDifference = 0 $Status = "ERROR" } else { $difference = New-TimeSpan -Start $System1DateTime.SystemDateTime -End $System2DateTime.SystemDateTime $DateTimeDifference = $difference $Status = 'Success' } $System1DateTime | Format-Table -AutoSize | Out-String | Write-Verbose $System2DateTime | Format-Table -AutoSize | Out-String | Write-Verbose return [PSCustomObject]@{ System1Name = $System1Name System1DateTime = $System1DateTime.SystemDateTime System2Name = $System2Name System2DateTime = $System2DateTime.SystemDateTime Type = $HealthCheckType Status = $Status Difference = $DateTimeDifference LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = @($System1DateTime.Comment,$System2DateTime.Comment) } } #Requires -Version 5 <# .Synopsis Test whether a web endpoint is reachable and returns HTTP 200. .Description Sends an HTTP request to the given URI using Invoke-WebRequest. Returns 'OK' when the server responds with a 200 status code, or an error/status description when the response indicates a problem. Exceptions (connection refused, DNS failure, etc.) are caught and returned as 'ERROR' results. .Parameter URI The full URI to request, e.g. 'http://server/health'. .Parameter SystemName Friendly name for the system this check belongs to (used in reporting). .Parameter SystemDescription Short description of the system (used in reporting). .Parameter UseBasicParsing Pass $true to use basic parsing (avoids IE engine dependency on servers without a GUI). .Parameter UseDefaultCredentials Pass $true to send the current user's Windows credentials with the request. .Example Test-URIHealth -URI "http://server/health" #> function Test-URIHealth { [CmdletBinding()] param ( [string]$URI, [string]$SystemName, [string]$SystemDescription, [bool]$UseBasicParsing, [bool]$UseDefaultCredentials ) $HealthCheckType = 'URI' try { $WebRequestSplat = @{ URI = $URI UseBasicParsing = $UseBasicParsing UseDefaultCredentials = $UseDefaultCredentials } $response = Invoke-WebRequest @WebRequestSplat if (!$null -eq $response) { if ($response.StatusCode -eq '200') { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $URI Type = $HealthCheckType Status = 'OK' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = "" ComputerName = $ENV:COMPUTERNAME } } else { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $URI Type = $HealthCheckType Status = $response.StatusDescription LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $response.StatusCode ComputerName = $ENV:COMPUTERNAME } } } else { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $URI Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $Error[0].Exception.Message ComputerName = $ENV:COMPUTERNAME } } } catch { return [PSCustomObject]@{ SystemName = $SystemName SystemDescription = $SystemDescription Name = $URI Type = $HealthCheckType Status = 'ERROR' LastUpdate = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Comment = $_.Exception.Message ComputerName = $ENV:COMPUTERNAME } } } |