{"id":531425,"date":"2025-09-24T09:53:10","date_gmt":"2025-09-24T09:53:10","guid":{"rendered":"https:\/\/www.ninjaone.com\/?post_type=script_hub&#038;p=531425"},"modified":"2025-09-24T09:53:10","modified_gmt":"2025-09-24T09:53:10","slug":"verifier-les-performances-du-systeme-avec-powershell","status":"publish","type":"script_hub","link":"https:\/\/www.ninjaone.com\/fr\/script-hub\/verifier-les-performances-du-systeme-avec-powershell\/","title":{"rendered":"Comment v\u00e9rifier les performances du syst\u00e8me avec un script PowerShell"},"content":{"rendered":"<p>Le <strong>contr\u00f4le des performances des syst\u00e8mes<\/strong> est essentiel pour une gestion informatique proactive. Qu&rsquo;il s&rsquo;agisse de r\u00e9soudre les plaintes des utilisateurs finaux ou d&rsquo;\u00e9valuer <a href=\"https:\/\/www.ninjaone.com\/docs\/patch-management\/device-health\/\">l&rsquo;\u00e9tat des appareils<\/a> au sein d&rsquo;une entreprise, les professionnels de l&rsquo;informatique et les fournisseurs de services g\u00e9r\u00e9s (MSP) ont besoin de m\u00e9thodes fiables et automatis\u00e9es pour mesurer l&rsquo;utilisation des ressources du syst\u00e8me. Cet article explore un puissant script PowerShell, le troisi\u00e8me script d&rsquo;une s\u00e9rie d&rsquo;enqu\u00eates DEX en trois parties, qui capture des statistiques d\u00e9taill\u00e9es sur le processeur, la m\u00e9moire vive, le disque et le r\u00e9seau, les formate pour une sortie HTML et les stocke \u00e9ventuellement dans un champ personnalis\u00e9 NinjaOne WYSIWYG pour une visibilit\u00e9 \u00e0 long terme.<\/p>\n<h2>Contexte<\/h2>\n<p>Le script 3 s&rsquo;appuie sur le retour d&rsquo;information recueilli dans le script 1 du flux de travail de l&rsquo;enqu\u00eate DEX. Lorsque les donn\u00e9es soumises par l&rsquo;utilisateur sugg\u00e8rent une d\u00e9gradation de l&rsquo;exp\u00e9rience, ce script automatise la d\u00e9cision de lancer une v\u00e9rification compl\u00e8te des performances du syst\u00e8me. Il s&rsquo;agit d&rsquo;un outil hautement contextuel, parfait pour l&rsquo;analyse des causes profondes apr\u00e8s la d\u00e9tection d&rsquo;un d\u00e9calage, d&rsquo;une <a href=\"https:\/\/www.ninjaone.com\/fr\/blog\/comment-reduire-l-utilisation-du-processeur\/\">utilisation \u00e9lev\u00e9e des ressources<\/a> ou d&rsquo;autres plaintes.<\/p>\n<p>En automatisant les contr\u00f4les et en formatant les r\u00e9sultats pour les tableaux de bord NinjaOne, le script simplifie la collaboration entre les \u00e9quipes, les comparaisons historiques et les flux de travail de triage des tickets. Pour les MSP, cela \u00e9limine les conjectures li\u00e9es aux diagnostics \u00e0 distance et ajoute un contexte mesurable aux rapports subjectifs que les utilisateurs fournissent souvent.<\/p>\n<h2>Le script<\/h2>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"powershell\">#Requires -Version 5.1\r\n\r\n&lt;#\r\n.SYNOPSIS\r\n    Script 3 of 3 in the DEX Survey experience. Based on the feedback in Script 1, collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field.\r\n.DESCRIPTION\r\n    Script 3 of 3 in the DEX Survey experience. Based on the feedback in Script 1, collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field.\r\nBy using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https:\/\/www.ninjaone.com\/terms-of-use.\r\n    Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. \r\n    Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. \r\n    Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. \r\n    Warranty Disclaimer: The script is provided \u201cas is\u201d and \u201cas available\u201d, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. \r\n    Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. \r\n    Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. \r\n    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).\r\n.EXAMPLE\r\n    -DaysSinceLastReboot \"7\" -DurationToPerformTests \"5\" -NumberOfEvents \"5\" -WysiwygCustomField \"WYSIWYG\" -DisplayUserMessage\r\n\r\n    Gathering all user profiles.\r\n    Gathering each user's feedback log location.\r\n\r\n    Checking 'C:\\Users\\tuser1\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log' for user feedback.\r\n    New feedback has been found.\r\n\r\n    A system performance check is needed.\r\n\r\n    Checking for currently logged-in users.\r\n    Sending message to all users.\r\n    Sending message to session Console, display time 150\r\n    Async message sent to session Console\r\n    Sending message to session 31C5CE94259D4006A9E4#0, display time 150\r\n    Async message sent to session 31C5CE94259D4006A9E4#0\r\n    ExitCode: 0\r\n\r\n    Collecting event logs.\r\n    Searching for performance counter localizations.\r\n    Collecting performance metrics for 5 minutes.\r\n    [Warning] The data in one of the performance counter samples is not valid. View the Status property for each PerformanceCounterSample object to make sure it contains valid data.\r\n    [Warning] The data in one of the performance counter samples is not valid. View the Status property for each PerformanceCounterSample object to make sure it contains valid data.\r\n    [Warning] The data in one of the performance counter samples is not valid. View the Status property for each PerformanceCounterSample object to make sure it contains valid data.\r\n    [Warning] The data in one of the performance counter samples is not valid. View the Status property for each PerformanceCounterSample object to make sure it contains valid data.\r\n    [Warning] The data in one of the performance counter samples is not valid. View the Status property for each PerformanceCounterSample object to make sure it contains valid data.\r\n\r\n    ### 12th Gen Intel(R) Core(TM) i9-12900H 2.918 GHz ###\r\n    CPU Average % CPU Minimum % CPU Maximum %\r\n    ------------- ------------- -------------\r\n    41.11%        34.3%         50.31%\r\n\r\n    ### Memory Usage ###\r\n    Total Memory Installed: 4 GB\r\n    RAM Average % RAM Minimum % RAM Maximum %\r\n    ------------- ------------- -------------\r\n    45.27%        44.32%        46.36%\r\n\r\n    ### Top 5 CPU Processes ###\r\n    Process Name  Average CPU % Used Minimum CPU % Used Maximum CPU % Used\r\n    ------------  ------------------ ------------------ ------------------\r\n    tiworker      78.53%             65.87%             97.09%\r\n    ninjarmmagent 0.17%              0.1%               0.29%\r\n    conhost       0.09%              0.03%              0.21%\r\n    svchost       0.08%              0.05%              0.13%\r\n    msmpeng       0.08%              0.03%              0.16%\r\n\r\n    ### Top 5 RAM Processes ###\r\n    Process Name Average RAM % Used Minimum RAM % Used Maximum RAM % Used\r\n    ------------ ------------------ ------------------ ------------------\r\n    tiworker     13.53%             13.08%             14.06%\r\n    msmpeng      3.15%              2.85%              3.54%\r\n    svchost      2.38%              2.37%              2.39%\r\n    powershell   2.25%              2.25%              2.25%\r\n    dwm          0.32%              0.32%              0.32%\r\n\r\n    ### Network Usage ###\r\n    NetworkAdapter          : microsoft hyper-v network adapter\r\n    MacAddress              : 00-17-FB-00-00-00\r\n    Type                    : Wired\r\n    Average Sent &amp; Received : 0 Mbps\r\n    Minimum Sent &amp; Received : 0 Mbps\r\n    Maximum Sent &amp; Received : 0 Mbps\r\n\r\n    ### Disk Usage ###\r\n    DriveLetter  : C\r\n    FreeSpace    : 22.97 GB (46.44%)\r\n    TotalSpace   : 49.47 GB\r\n    PhysicalDisk : Msft Virtual Disk\r\n    MediaType    : Unspecified\r\n    Average IOPS : 83.07 IOPS\r\n    Minimum IOPS : 20.1 IOPS\r\n    Maximum IOPS : 119.06 IOPS\r\n\r\n    ### Top 5 IO Processes (Network &amp; Disk Combined) ###\r\n    Process Name  Average IO Used Minimum IO Used Maximum IO Used\r\n    ------------  --------------- --------------- ---------------\r\n    tiworker      24.8218 Mbps    19.1945 Mbps    31.2237 Mbps\r\n    wmiprvse      0.4951 Mbps     0 Mbps          2.4754 Mbps\r\n    svchost       0.3126 Mbps     0.297 Mbps      0.3608 Mbps\r\n    system        0.1814 Mbps     0.0042 Mbps     0.4999 Mbps\r\n    ninjarmmagent 0.0305 Mbps     0.018 Mbps      0.0766 Mbps\r\n\r\n    Retrieving WinSAT assessment data.\r\n    More info: https:\/\/learn.microsoft.com\/en-us\/previous-versions\/windows\/it-pro\/windows-8.1-and-8\/hh825488(v=win.10)\r\n    ExitCode: 0\r\n    Retrieving WinSAT assessment data.\r\n    Successfully retrieved assessment data.\r\n\r\n    Attempting to set Custom Field 'WYSIWYG'.\r\n    Successfully set Custom Field 'WYSIWYG'!\r\n\r\n    Checking for currently logged-in users.\r\n\r\n    Sending message to all users.\r\n    Sending message to session Console, display time 150\r\n    Async message sent to session Console\r\n    Sending message to session 31C5CE94259D4006A9E4#0, display time 150\r\n    Async message sent to session 31C5CE94259D4006A9E4#0\r\n    ExitCode: 0\r\n\r\n    Updating the feedback log at 'C:\\Users\\tuser1\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log'.\r\n    Updated the log file at 'C:\\Users\\tuser1\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log'.\r\n\r\n    ### Last 2147483646 errors in Application, Security, Setup and System Log. ###\r\n    LogName      : Application\r\n    ProviderName : Microsoft-Windows-Defrag\r\n    Id           : 257\r\n    TimeCreated  : 5\/20\/2025 3:35:45 PM\r\n    Message      : The volume Windows RE Tools was not optimized because an error\r\n                was encountered: Neither Slab Consolidation nor Slab Analysis\r\n                will run if slabs are less than 8 MB. (0x8900002D)\r\n    ....\r\n\r\n.PARAMETER DisplayUserMessage\r\n    Display a message to the end-user informing them that you are collecting performance metrics and that they should not restart the computer.\r\n\r\n.PARAMETER DaysSinceLastReboot\r\n    Specify the number of days by which the system should have been rebooted.\r\n\r\n.PARAMETER DurationToPerformTests\r\n    The duration (in minutes) for which the performance tests should be executed.\r\n\r\n.PARAMETER NumberOfEvents\r\n    The number of error events to retrieve from the Application, Security, Setup, and System event logs.\r\n\r\n.PARAMETER WysiwygCustomField\r\n    Optionally specify the name of a WYSIWYG custom field to store the formatted performance data.\r\n\r\n.NOTES\r\n    Minimum OS Architecture Supported: Windows 10, Windows Server 2016\r\n    Release Notes: Initial Release\r\n#&gt;\r\n\r\n[CmdletBinding()]\r\nparam (\r\n    [Parameter()]\r\n    $DaysSinceLastReboot,\r\n    [Parameter()]\r\n    $DurationToPerformTests = 5,\r\n    [Parameter()]\r\n    $NumberOfEvents,\r\n    [Parameter()]\r\n    [String]$WysiwygCustomField,\r\n    [Parameter()]\r\n    [Switch]$DisplayUserMessage = [System.Convert]::ToBoolean($env:displayUserMessage)\r\n)\r\n\r\nbegin {\r\n    # If script form variables are used, replace command line parameters with their values.\r\n    if ($env:daysSinceLastReboot) { $DaysSinceLastReboot = $env:daysSinceLastReboot }\r\n    if ($env:durationToPerformTests) { $DurationToPerformTests = $env:durationToPerformTests }\r\n    if ($env:numberOfEvents) { $NumberOfEvents = $env:numberOfEvents }\r\n    if ($env:wysiwygCustomFieldName) { $WysiwygCustomField = $env:wysiwygCustomFieldName }\r\n\r\n    # Check if the operating system build version is less than 10240 (Windows 10 or Windows Server 2016 minimum requirement)\r\n    if ([System.Environment]::OSVersion.Version.Build -lt 10240) {\r\n        Write-Host -Object \"`n[Warning] OS build '$([System.Environment]::OSVersion.Version.Build)' detected.\"\r\n        Write-Host -Object \"[Warning] The minimum OS version supported by this script is Windows 10 (10240) or Windows Server 2016 (14393).`n\"\r\n    }\r\n\r\n    # Ensure the value does not contain invalid characters.\r\n    if ($DaysSinceLastReboot -and $DaysSinceLastReboot -match \"[^0-9]\") {\r\n        Write-Host -Object \"[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. It contains invalid characters.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n        exit 1\r\n    }\r\n\r\n    # Validate the 'Days Since Last Reboot' input.\r\n    if ($DaysSinceLastReboot) {\r\n        try {\r\n            # Attempt to cast the value to a number.\r\n            [int]$DaysSinceLastReboot = $DaysSinceLastReboot\r\n        } catch {\r\n            # If the conversion fails, display an error message and exit the script.\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid.\"\r\n            Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n            exit 1\r\n        }\r\n    }\r\n\r\n    # Ensure the value is non-negative (greater than or equal to 0).\r\n    if ($DaysSinceLastReboot -and $DaysSinceLastReboot -lt 0) {\r\n        Write-Host -Object \"[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. It is less than 0.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n        exit 1\r\n    }\r\n\r\n    # Ensure the value is less than the maximum allowed value.\r\n    if ($DaysSinceLastReboot -and $DaysSinceLastReboot -ge [int]::MaxValue) {\r\n        Write-Host -Object \"[Error] The 'Days Since Last Reboot' value of '$DaysSinceLastReboot' is invalid. It is greater than or equal to '$([int]::MaxValue)'.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n        exit 1\r\n    }\r\n\r\n    # Validate the 'Duration To Perform Tests' input.\r\n    if ([string]::IsNullOrWhiteSpace($DurationToPerformTests)) {\r\n        Write-Host -Object \"[Error] Please provide the duration for which you would like to perform the tests using the 'Duration To Perform Tests' box.\"\r\n        exit 1\r\n    }\r\n\r\n    # Ensure the value does not contain invalid characters.\r\n    if ($DurationToPerformTests -match \"[^0-9]\") {\r\n        Write-Host -Object \"[Error] The 'Duration To Perform Tests' value of '$DurationToPerformTests' is invalid. It contains invalid characters.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that's greater than 0 and less than or equal to 60.\"\r\n        exit 1\r\n    }\r\n\r\n    # Validate the 'Duration to Perform Tests input' input.\r\n    if (![string]::IsNullOrWhiteSpace($DurationToPerformTests)) {\r\n        try {\r\n            # Attempt to cast the value to a number.\r\n            [int]$DurationToPerformTests = $DurationToPerformTests\r\n        } catch {\r\n            # If the conversion fails, display an error message and exit the script.\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] The 'Duration to Perform Tests' value of '$DurationToPerformTests' is invalid.\"\r\n            Write-Host -Object \"[Error] Please provide a positive whole number that is greater than 0 and less than or equal to 60.\"\r\n            exit 1\r\n        }\r\n    }\r\n\r\n    # Ensure the duration is between 1 and 60.\r\n    if ($DurationToPerformTests -lt 1 -or $DurationToPerformTests -gt 60) {\r\n        Write-Host -Object \"[Error] The 'Duration To Perform Tests' value of '$DurationToPerformTests' is invalid. It is less than 1 or greater than 60.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that's greater than 0 and less than or equal to 60.\"\r\n        exit 1\r\n    }\r\n\r\n    # Ensure the value does not contain invalid characters.\r\n    if ($NumberOfEvents -and $NumberOfEvents -match \"[^0-9]\") {\r\n        Write-Host -Object \"[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. It contains invalid characters.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n        exit 1\r\n    }\r\n\r\n    # Validate the 'Number of Events' input.\r\n    if ($NumberOfEvents) {\r\n        try {\r\n            # Attempt to cast the value to a number.\r\n            [int]$NumberOfEvents = $NumberOfEvents\r\n        } catch {\r\n            # If the conversion fails, display an error message and exit the script.\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid.\"\r\n            Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n            exit 1\r\n        }\r\n    }\r\n\r\n    # Ensure the value is non-negative (greater than or equal to 0).\r\n    if ($NumberOfEvents -and $NumberOfEvents -lt 0) {\r\n        Write-Host -Object \"[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. It is less than 0.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n        exit 1\r\n    }\r\n\r\n    # Ensure the value is less than the maximum allowed value.\r\n    if ($NumberOfEvents -and $NumberOfEvents -ge [int]::MaxValue) {\r\n        Write-Host -Object \"[Error] The 'Number of Events' value of '$NumberOfEvents' is invalid. It is greater than or equal to '$([int]::MaxValue)'.\"\r\n        Write-Host -Object \"[Error] Please provide a positive whole number that is greater than or equal to 0 and less than '$([int]::MaxValue)'.\"\r\n        exit 1\r\n    }\r\n\r\n    function Invoke-LegacyConsoleTool {\r\n        [CmdletBinding()]\r\n        param(\r\n            [Parameter()]\r\n            [String]$FilePath,\r\n            [Parameter()]\r\n            [String[]]$ArgumentList,\r\n            [Parameter()]\r\n            [Int]$Timeout = 30,\r\n            [Parameter()]\r\n            [System.Text.Encoding]$Encoding\r\n        )\r\n\r\n        # Validate that the file path is not null or empty\r\n        if ([String]::IsNullOrWhiteSpace($FilePath)) {\r\n            throw (New-Object System.ArgumentNullException(\"You must provide a file path to the legacy tool you are trying to use.\"))\r\n        }\r\n\r\n        # Validate that a timeout value is provided\r\n        if (!$Timeout) {\r\n            throw (New-Object System.ArgumentNullException(\"You must provide a timeout value.\"))\r\n        }\r\n\r\n        # Check if the file path is not rooted and does not exist in the current directory\r\n        if (!([System.IO.Path]::IsPathRooted($FilePath)) -and !(Test-Path -Path $FilePath -PathType Leaf -ErrorAction SilentlyContinue)) {\r\n            # Retrieve the system PATH environment variable and split it into directories\r\n            $EnvPaths = [System.Environment]::GetEnvironmentVariable(\"PATH\").Split(\";\")\r\n            # Retrieve the PATHEXT environment variable to get executable file extensions\r\n            $PathExts = [System.Environment]::GetEnvironmentVariable(\"PATHEXT\").Split(\";\")\r\n\r\n            $ResolvedPath = $null\r\n            # Iterate through each directory in the PATH environment variable\r\n            foreach ($Directory in $EnvPaths) {\r\n                # Check for each possible file extension in PATHEXT\r\n                foreach ($FileExtension in $PathExts) {\r\n                    # Construct the potential file path\r\n                    $PotentialMatch = Join-Path $Directory ($FilePath + $FileExtension)\r\n                    # If the file exists, set it as the resolved path\r\n                    if (Test-Path $PotentialMatch -PathType Leaf) {\r\n                        $ResolvedPath = $PotentialMatch\r\n                        break\r\n                    }\r\n                }\r\n                # Exit the loop if a resolved path is found\r\n                if ($ResolvedPath) { break }\r\n            }\r\n\r\n            # If a resolved path is found, update the FilePath variable\r\n            if ($ResolvedPath) {\r\n                $FilePath = $ResolvedPath\r\n            }\r\n        }\r\n\r\n        # Check if the file exists at the specified path\r\n        if (!(Test-Path -Path $FilePath -PathType Leaf -ErrorAction SilentlyContinue)) {\r\n            throw (New-Object System.IO.FileNotFoundException(\"Unable to find '$FilePath'.\"))\r\n        }\r\n\r\n        # Ensure the timeout value is at least 30 seconds\r\n        if ($Timeout -lt 30) {\r\n            throw (New-Object System.ArgumentOutOfRangeException(\"You must provide a timeout value that is greater than or equal to 30 seconds.\"))\r\n        }\r\n\r\n        # Initialize a ProcessStartInfo object to configure the process\r\n        $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo\r\n        $ProcessInfo.FileName = $FilePath\r\n\r\n        # Combine arguments into a single string if provided\r\n        if ($ArgumentList) {\r\n            $ProcessInfo.Arguments = $ArgumentList -join \" \"\r\n        }\r\n\r\n        # Configure the process to run without a shell, without a new window, and with redirected I\/O\r\n        $ProcessInfo.UseShellExecute = $False\r\n        $ProcessInfo.CreateNoWindow = $True\r\n        $ProcessInfo.RedirectStandardInput = $True\r\n        $ProcessInfo.RedirectStandardOutput = $True\r\n        $ProcessInfo.RedirectStandardError = $True\r\n\r\n        # Set the encoding for standard output and error streams\r\n        if (!$Encoding) {\r\n            try {\r\n                # Dynamically load the method to get the OEM code page if not already loaded\r\n                if (-not ([System.Management.Automation.PSTypeName]'NativeMethods.Win32').Type) {\r\n                    $Definition = '[DllImport(\"kernel32.dll\")]' + \"`n\" + 'public static extern uint GetOEMCP();'\r\n                    Add-Type -MemberDefinition $Definition -Name \"Win32\" -Namespace \"NativeMethods\" -ErrorAction Stop\r\n                }\r\n\r\n                # Retrieve the OEM code page and set the encoding\r\n                [int]$OemCodePage = [NativeMethods.Win32]::GetOEMCP()\r\n                $Encoding = [System.Text.Encoding]::GetEncoding($OemCodePage)\r\n            } catch {\r\n                throw $_\r\n            }\r\n        }\r\n        $ProcessInfo.StandardOutputEncoding = $Encoding\r\n        $ProcessInfo.StandardErrorEncoding = $Encoding\r\n\r\n        # Create a new process object and attach the ProcessStartInfo configuration\r\n        $Process = New-Object System.Diagnostics.Process\r\n        $Process.StartInfo = $ProcessInfo\r\n\r\n        # Add properties to store standard output and error streams\r\n        $Process | Add-Member -MemberType NoteProperty -Name StdOut -Value (New-Object System.Collections.Generic.List[string]) -Force | Out-Null\r\n        $Process | Add-Member -MemberType NoteProperty -Name StdErr -Value (New-Object System.Collections.Generic.List[string]) -Force | Out-Null\r\n\r\n        # Start the process\r\n        $Process.Start() | Out-Null\r\n\r\n        $ProcessTimeout = 0\r\n        $TimeoutInMilliseconds = $Timeout * 1000\r\n\r\n        # Initialize string builders to collect output\r\n        $StdOutBuffer = New-Object System.Text.StringBuilder\r\n        $StdErrBuffer = New-Object System.Text.StringBuilder\r\n\r\n        # Monitor the process execution and read output\/error streams\r\n        while (!$Process.HasExited -and $ProcessTimeout -lt $TimeoutInMilliseconds ) {\r\n            # Read standard output to prevent buffer overflow\r\n            while (!$Process.StandardOutput.EndOfStream -and $Process.StandardOutput.Peek() -ne -1) {\r\n                $Char = $Process.StandardOutput.Read()\r\n                if ($Char -ne -1) {\r\n                    $ActualCharacter = [char]$Char\r\n                    if ($ActualCharacter -eq \"`n\") {\r\n                        # Add the completed line to the StdOut collection\r\n                        $Process.StdOut.Add($StdOutBuffer.ToString())\r\n                        $null = $StdOutBuffer.Clear()\r\n                    } elseif ($ActualCharacter -ne \"`r\") {\r\n                        # Append characters to the buffer, excluding carriage returns\r\n                        $null = $StdOutBuffer.Append($ActualCharacter)\r\n                    }\r\n                }\r\n            }\r\n\r\n            # Read standard error to prevent buffer overflow\r\n            while (!$Process.StandardError.EndOfStream -and $Process.StandardError.Peek() -ne -1) {\r\n                $Char = $Process.StandardError.Read()\r\n                if ($Char -ne -1) {\r\n                    $ActualCharacter = [char]$Char\r\n                    if ($ActualCharacter -eq \"`n\") {\r\n                        # Add the completed line to the StdErr collection\r\n                        $Process.StdErr.Add($StdErrBuffer.ToString())\r\n                        $null = $StdErrBuffer.Clear()\r\n                    } elseif ($ActualCharacter -ne \"`r\") {\r\n                        # Append characters to the buffer, excluding carriage returns\r\n                        $null = $StdErrBuffer.Append($ActualCharacter)\r\n                    }\r\n                }\r\n            }\r\n\r\n            # Sleep briefly before polling again to avoid excessive CPU usage\r\n            Start-Sleep -Milliseconds 100\r\n            $ProcessTimeout = $ProcessTimeout + 10\r\n        }\r\n\r\n        # Add final buffered content to StdOut and StdErr properties\r\n        if ($StdOutBuffer.Length -gt 0) {\r\n            $Process.StdOut.Add($StdOutBuffer.ToString())\r\n        }\r\n\r\n        if ($StdErrBuffer.Length -gt 0) {\r\n            $Process.StdErr.Add($StdErrBuffer.ToString())\r\n        }\r\n\r\n        try {\r\n            # Handle timeout scenarios\r\n            if ($ProcessTimeout -ge 300000) {\r\n                throw (New-Object System.ServiceProcess.TimeoutException(\"The process has timed out.\"))\r\n            }\r\n\r\n            # Wait for the process to exit within the remaining timeout period\r\n            $TimeoutRemaining = 300000 - $ProcessTimeout\r\n            if (!$Process.WaitForExit($TimeoutRemaining)) {\r\n                throw (New-Object System.ServiceProcess.TimeoutException(\"The process has timed out.\"))\r\n            }\r\n        } catch {\r\n            # Set the global exit code and dispose of the process\r\n            if ($Process.ExitCode) {\r\n                $GLOBAL:LASTEXITCODE = $Process.ExitCode\r\n            } else {\r\n                $GLOBAL:LASTEXITCODE = 1\r\n            }\r\n\r\n            # Dispose of the process to release resources\r\n            if ($Process) {\r\n                $Process.Dispose()\r\n            }\r\n\r\n            throw $_\r\n        }\r\n\r\n        # Final read of output and error streams to ensure all data is captured\r\n        while (!$Process.StandardOutput.EndOfStream) {\r\n            $Char = $Process.StandardOutput.Read()\r\n            if ($Char -ne -1) {\r\n                $ActualCharacter = [char]$Char\r\n                if ($ActualCharacter -eq \"`n\") {\r\n                    # Add the completed line to the StdOut collection\r\n                    $Process.StdOut.Add($StdOutBuffer.ToString())\r\n                    $null = $StdOutBuffer.Clear()\r\n                } elseif ($ActualCharacter -ne \"`r\") {\r\n                    # Append characters to the buffer, excluding carriage returns\r\n                    $null = $StdOutBuffer.Append($ActualCharacter)\r\n                }\r\n            }\r\n        }\r\n\r\n        while (!$Process.StandardError.EndOfStream) {\r\n            $Char = $Process.StandardError.Read()\r\n            if ($Char -ne -1) {\r\n                $ActualCharacter = [char]$Char\r\n                if ($ActualCharacter -eq \"`n\") {\r\n                    # Add the completed line to the StdErr collection\r\n                    $Process.StdErr.Add($StdErrBuffer.ToString())\r\n                    $null = $StdErrBuffer.Clear()\r\n                } elseif ($ActualCharacter -ne \"`r\") {\r\n                    # Append characters to the buffer, excluding carriage returns\r\n                    $null = $StdErrBuffer.Append($ActualCharacter)\r\n                }\r\n            }\r\n        }\r\n\r\n        if ($Process.StdErr.Count -gt 0) {\r\n            # Set the global exit code\r\n            if ($Process.ExitCode -or $Process.ExitCode -eq 0) {\r\n                $GLOBAL:LASTEXITCODE = $Process.ExitCode\r\n            }\r\n\r\n            # Dispose of the process\r\n            if ($Process) {\r\n                $Process.Dispose()\r\n            }\r\n\r\n            # Log errors from the standard error stream\r\n            $Process.StdErr | Write-Error -Category \"FromStdErr\"\r\n        }\r\n\r\n        # Return the standard output if available\r\n        if ($Process.StdOut.Count -gt 0) {\r\n            $Process.StdOut\r\n        }\r\n\r\n        # Set the global exit code\r\n        if ($Process.ExitCode -or $Process.ExitCode -eq 0) {\r\n            $GLOBAL:LASTEXITCODE = $Process.ExitCode\r\n        }\r\n\r\n        # Dispose of the process\r\n        if ($Process) {\r\n            $Process.Dispose()\r\n        }\r\n    }\r\n\r\n    function Test-IsServer {\r\n        # Determine the method to retrieve the operating system information based on PowerShell version\r\n\r\n        try {\r\n            $OS = if ($PSVersionTable.PSVersion.Major -lt 3) {\r\n                Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop\r\n            } else {\r\n                Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop\r\n            }\r\n        } catch {\r\n            Write-Host -Object \"[Error] Failed to identity if this device is a workstation or server.\"\r\n            throw $_\r\n        }\r\n\r\n        # Check if the ProductType is \"3\" or \"2\", which indicates that the system is a server\r\n        if ($OS.ProductType -eq \"3\" -or $OS.ProductType -eq \"2\") {\r\n            return $true\r\n        }\r\n    }\r\n\r\n    # Check if the script is running on a server.\r\n    try {\r\n        $IsServer = Test-IsServer\r\n    } catch {\r\n        Write-Host -Object \"[Error] Unable to identify device type.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)`n\"\r\n        $ExitCode = 1\r\n    }\r\n\r\n    if ($IsServer -and $DisplayUserMessage) {\r\n        # Attempt to check if the RDS role is installed.\r\n        try {\r\n            # Retrieve the RDS role feature and check if it is installed.\r\n            $RDSRole = Get-WindowsFeature -Name RDS-RD-Server | Where-Object { $_.Installed }\r\n        } catch {\r\n            # If an error occurs during the check, output an error message and exit the script.\r\n            Write-Host -Object \"[Error] Unable to check if the RDS role is installed.\"\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            exit 1\r\n        }\r\n\r\n        # If the RDS role is installed, output an error message and exit the script.\r\n        if ($RDSRole) {\r\n            Write-Host -Object \"[Error] This script doesn't support sending a message on RDS servers because the message would show for all logged-in users, potentially creating a source of confusion.\"\r\n            exit 1\r\n        }\r\n    }\r\n\r\n    function Test-IsElevated {\r\n        [CmdletBinding()]\r\n        param ()\r\n\r\n        # Get the current Windows identity of the user running the script\r\n        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()\r\n\r\n        # Create a WindowsPrincipal object based on the current identity\r\n        $p = New-Object System.Security.Principal.WindowsPrincipal($id)\r\n\r\n        # Check if the current user is in the Administrator role\r\n        # The function returns $True if the user has administrative privileges, $False otherwise\r\n        # 544 is the value for the Built-In Administrators role\r\n        # Reference: https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.security.principal.windowsbuiltinrole\r\n        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544')\r\n    }\r\n\r\n    function Get-UserHive {\r\n        [CmdletBinding()]\r\n        param (\r\n            [Parameter()]\r\n            [ValidateSet('AzureAD', 'DomainAndLocal', 'All')]\r\n            [String]$Type = \"All\",\r\n            [Parameter()]\r\n            [String[]]$ExcludedUsers,\r\n            [Parameter()]\r\n            [switch]$IncludeDefault\r\n        )\r\n\r\n        # Define the SID patterns to match based on the selected user type\r\n        $Patterns = switch ($Type) {\r\n            \"AzureAD\" { \"S-1-12-1-(\\d+-?){4}$\" }\r\n            \"DomainAndLocal\" { \"S-1-5-21-(\\d+-?){4}$\" }\r\n            \"All\" { \"S-1-12-1-(\\d+-?){4}$\" ; \"S-1-5-21-(\\d+-?){4}$\" }\r\n        }\r\n\r\n        # Retrieve user profile information based on the defined patterns\r\n        $UserProfiles = Foreach ($Pattern in $Patterns) {\r\n            Get-ItemProperty \"Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\*\" |\r\n                Where-Object { $_.PSChildName -match $Pattern } |\r\n                Select-Object @{Name = \"SID\"; Expression = { $_.PSChildName } },\r\n                @{Name = \"Username\"; Expression = { \"$($_.ProfileImagePath | Split-Path -Leaf)\" } },\r\n                @{Name = \"Domain\"; Expression = { if ($_.PSChildName -match \"S-1-12-1-(\\d+-?){4}$\") { \"AzureAD\" }else { $Null } } },\r\n                @{Name = \"UserHive\"; Expression = { \"$($_.ProfileImagePath)\\NTuser.dat\" } },\r\n                @{Name = \"Path\"; Expression = { $_.ProfileImagePath } }\r\n        }\r\n\r\n        # If the IncludeDefault switch is set, add the Default profile to the results\r\n        switch ($IncludeDefault) {\r\n            $True {\r\n                $DefaultProfile = \"\" | Select-Object Username, SID, UserHive, Path\r\n                $DefaultProfile.Username = \"Default\"\r\n                $DefaultProfile.Domain = $env:COMPUTERNAME\r\n                $DefaultProfile.SID = \"DefaultProfile\"\r\n                $DefaultProfile.Userhive = \"$env:SystemDrive\\Users\\Default\\NTUSER.DAT\"\r\n                $DefaultProfile.Path = \"C:\\Users\\Default\"\r\n\r\n                # Exclude users specified in the ExcludedUsers list\r\n                $DefaultProfile | Where-Object { $ExcludedUsers -notcontains $_.Username }\r\n            }\r\n        }\r\n\r\n        if ($PSVersionTable.PSVersion.Major -lt 3) {\r\n            $AllAccounts = Get-WmiObject -Class \"win32_UserAccount\"\r\n        } else {\r\n            $AllAccounts = Get-CimInstance -ClassName \"win32_UserAccount\"\r\n        }\r\n\r\n        $CompleteUserProfiles = $UserProfiles | ForEach-Object {\r\n            $SID = $_.SID\r\n            $Win32Object = $AllAccounts | Where-Object { $_.SID -like $SID }\r\n\r\n            if ($Win32Object) {\r\n                $Win32Object | Add-Member -NotePropertyName UserHive -NotePropertyValue $_.UserHive\r\n                $Win32Object | Add-Member -NotePropertyName Path -NotePropertyValue $_.Path\r\n                $Win32Object\r\n            } else {\r\n                [PSCustomObject]@{\r\n                    Name     = $_.Username\r\n                    Domain   = $_.Domain\r\n                    SID      = $_.SID\r\n                    UserHive = $_.UserHive\r\n                    Path     = $_.Path\r\n                }\r\n            }\r\n        }\r\n\r\n        # Return the list of user profiles, excluding any specified in the ExcludedUsers list\r\n        $CompleteUserProfiles | Where-Object { $ExcludedUsers -notcontains $_.Name }\r\n    }\r\n\r\n    if (!$ExitCode) {\r\n        $ExitCode = 0\r\n    }\r\n\r\n    $StartedDateTime = Get-Date\r\n}\r\nprocess {\r\n    # Attempt to determine if the current session is running with Administrator privileges.\r\n    try {\r\n        $IsElevated = Test-IsElevated -ErrorAction Stop\r\n    } catch {\r\n        # Log an error if unable to determine admin privileges\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        Write-Host -Object \"[Error] Unable to determine if the account '$env:Username' is running with Administrator privileges.\"\r\n        exit 1\r\n    }\r\n\r\n    # Exit if the script is not running with Administrator privileges\r\n    if (!($IsElevated)) {\r\n        Write-Host -Object \"[Error] Access Denied: The user '$env:Username' does not have administrator privileges, or the script is not running with elevated permissions.\"\r\n        exit 1\r\n    }\r\n\r\n    # Check if the script is being run with elevated (Administrator) privileges.\r\n    # If not, display an error message and exit the script.\r\n    if (!(Test-IsElevated)) {\r\n        Write-Host -Object \"[Error] Access Denied. Please run with Administrator privileges.\"\r\n        exit 1\r\n    }\r\n\r\n    # Check if the lock file exists to prevent multiple instances of the script from running.\r\n    # If it exists, read the process ID from the lock file and check if the process is still running.\r\n    if (Test-Path -Path \"$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt\" -ErrorAction SilentlyContinue) {\r\n        try {\r\n            Write-Host -Object \"Process lock file found at '$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt'. Checking if the process is still running.\"\r\n\r\n            # Retrieve the process ID from the lock file.\r\n            $OtherScript = Get-Content -Path \"$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt\" -ErrorAction Stop\r\n\r\n            # Check if the process ID exists, indicating the script is already running.\r\n            if (Get-Process -Id $OtherScript -ErrorAction SilentlyContinue) {\r\n                Write-Host -Object \"[Error] This script is already running in another process with the process id (PID) '$OtherScript'.\"\r\n                exit 1\r\n            }\r\n        } catch {\r\n            # If there is an error accessing the lock file, display an error message and exit.\r\n            Write-Host -Object \"[Error] Unable to access the lock file at '$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt'.\"\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            exit 1\r\n        }\r\n    }\r\n\r\n    # Attempt to write the current process ID to the lock file, preventing multiple instances of the script from running.\r\n    try {\r\n        [System.Diagnostics.Process]::GetCurrentProcess().Id | Out-File -FilePath \"$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt\" -Force -ErrorAction Stop\r\n    } catch {\r\n        # If the lock file cannot be created, display an error message and exit.\r\n        Write-Host -Object \"[Error] Failed to create lock file at '$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt'.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        exit 1\r\n    }\r\n\r\n    try {\r\n        # Retrieve all user profiles on the system\r\n        Write-Host -Object \"Gathering all user profiles.\"\r\n        $UserProfiles = Get-UserHive -Type \"All\" -ErrorAction Stop\r\n        $ProfileWasLoaded = New-Object System.Collections.Generic.List[object]\r\n    } catch {\r\n        # Log an error if unable to gather user profiles\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        Write-Host -Object \"[Error] Failed to gather all the user profiles.\"\r\n        exit 1\r\n    }\r\n\r\n    # Loop through each user profile to load their registry hive if not already loaded\r\n    ForEach ($UserProfile in $UserProfiles) {\r\n        If (!(Test-Path -Path Registry::HKEY_USERS\\$($UserProfile.SID) -ErrorAction SilentlyContinue)) {\r\n            Start-Process -FilePath \"cmd.exe\" -ArgumentList \"\/C reg.exe LOAD HKU\\$($UserProfile.SID) `\"$($UserProfile.UserHive)`\"\" -Wait -WindowStyle Hidden\r\n            $ProfileWasLoaded.Add($UserProfile)\r\n        }\r\n    }\r\n\r\n    # Locate feedback log files for each user profile\r\n    Write-Host -Object \"Gathering each user's feedback log location.\"\r\n    $FeedbackLogLocations = New-Object System.Collections.Generic.List[object]\r\n    $UserProfiles | ForEach-Object {\r\n        # Check for feedback log file in various locations\r\n        $AppDataLocation = Get-ItemProperty -Path \"Registry::HKEY_USERS\\$($_.SID)\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty \"Local AppData\" -ErrorAction SilentlyContinue\r\n\r\n        if ($AppDataLocation -and (Test-Path -Path \"$AppDataLocation\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\" -Type Leaf -ErrorAction SilentlyContinue)) {\r\n            $FeedbackLogLocations.Add(\r\n                [PSCustomObject]@{\r\n                    Name            = $_.Name\r\n                    Domain          = $_.Domain\r\n                    LogFileLocation = \"$AppDataLocation\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\"\r\n                }\r\n            )\r\n            return\r\n        }\r\n\r\n        if (Test-Path -Path \"$($_.Path)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\" -Type Leaf -ErrorAction SilentlyContinue) {\r\n            $FeedbackLogLocations.Add(\r\n                [PSCustomObject]@{\r\n                    Name            = $_.Name\r\n                    Domain          = $_.Domain\r\n                    LogFileLocation = \"$($_.Path)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\"\r\n                }\r\n            )\r\n            return\r\n        }\r\n\r\n        if (Test-Path -Path \"C:\\Users\\$($_.Name)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\" -Type Leaf -ErrorAction SilentlyContinue) {\r\n            $FeedbackLogLocations.Add(\r\n                [PSCustomObject]@{\r\n                    Name            = $_.Name\r\n                    Domain          = $_.Domain\r\n                    LogFileLocation = \"C:\\Users\\$($_.Name)\\AppData\\Local\\NinjaOne-DEX-Survey-Script\\UserDeviceFeedback.log\"\r\n                }\r\n            )\r\n            return\r\n        }\r\n\r\n        # Log a message if no feedback log file is found for the user\r\n        Write-Host -Object \"No feedback log file found for $($_.Name) in the domain $($_.Domain).\"\r\n    }\r\n\r\n    # Unload user registry hives if they were loaded during this script execution\r\n    if ($ProfileWasLoaded.Count -gt 0) {\r\n        ForEach ($UserProfile in $ProfileWasLoaded) {\r\n            [gc]::Collect()\r\n            Start-Sleep 1\r\n            Start-Process -FilePath \"cmd.exe\" -ArgumentList \"\/C reg.exe UNLOAD HKU\\$($UserProfile.SID)\" -Wait -WindowStyle Hidden | Out-Null\r\n        }\r\n    }\r\n\r\n    # Log a warning if no feedback log files were found\r\n    if ($FeedbackLogLocations.Count -lt 1) {\r\n        Write-Host -Object \"[Warning] No feedback was found.\"\r\n    }\r\n\r\n    Write-Host -Object \"\"\r\n\r\n    # Iterate through each feedback log file location\r\n    foreach ($LogFile in $FeedbackLogLocations) {\r\n        # Inform the user that the feedback log file is being checked\r\n        Write-Host -Object \"Checking '$($LogFile.LogFileLocation)' for user feedback.\"\r\n\r\n        try {\r\n            # Attempt to read the last 200,000 lines of the feedback log file\r\n            $LogContent = Get-Content -Path $LogFile.LogFileLocation -Tail 200000 -ErrorAction Stop\r\n        } catch {\r\n            # Log an error if the feedback log file cannot be read\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to retrieve the feedback log file contents for the user '$($LogFile.Name)'.\"\r\n            $ExitCode = 1\r\n            continue\r\n        }\r\n\r\n        $i = 0\r\n        # Iterate through each line in the feedback log content\r\n        foreach ($Line in $LogContent) {\r\n            $i++\r\n            # Split the line into data points using the '|' delimiter, expecting 6 parts\r\n            $DataPoints = $Line -split '\\|', 6\r\n\r\n            # Warn if the line has fewer than 6 data points\r\n            if ($DataPoints.Count -lt 6) {\r\n                Write-Host -Object \"[Warning] Only $($DataPoints.Count) data points have been found for line '$i'.\"\r\n                continue\r\n            }\r\n\r\n            # Skip lines where the fifth data point is not False\r\n            if ($DataPoints[4] -ne $False) {\r\n                continue\r\n            } else {\r\n                # If new feedback is found, set the flag and exit the loop\r\n                Write-Host -Object \"New feedback has been found.`n\"\r\n                $SystemPerformanceCheckNeeded = $True\r\n                break\r\n            }\r\n        }\r\n\r\n        # Exit the outer loop if a system performance check is needed\r\n        if ($SystemPerformanceCheckNeeded) {\r\n            break\r\n        }\r\n    }\r\n\r\n    # Check if there is no feedback collected\r\n    if (!$SystemPerformanceCheckNeeded) {\r\n        Write-Host -Object \"Based on the current feedback, a system performance check is not needed.\"\r\n\r\n        # Try to remove the lock file to ensure no other instance of the script is running.\r\n        try {\r\n            Remove-Item -Path \"$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt\" -Force -ErrorAction Stop\r\n        } catch {\r\n            # If the removal of the lock file fails, catch the exception and display error messages.\r\n            Write-Host -Object \"[Error] Failed to remove the lock file at '$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt'.\"\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            $ExitCode = 1\r\n        }\r\n        exit $ExitCode\r\n    } else {\r\n        Write-Host -Object \"A system performance check is needed.`n\"\r\n    }\r\n\r\n    $TotalMessageTime = $($DurationToPerformTests * 60 \/ 2)\r\n    $TotalCollectionTime = $DurationToPerformTests\r\n\r\n    if ($DisplayUserMessage) {\r\n        try {\r\n            Write-Host -Object \"Checking for currently logged-in users.\"\r\n            $QuserOutput = Invoke-LegacyConsoleTool -FilePath \"$env:WINDIR\\System32\\quser.exe\" -ErrorVariable \"QUserErrors\" -ErrorAction SilentlyContinue\r\n            if ($QUserErrors) {\r\n                $QUserErrors | ForEach-Object { Write-Host -Object \"[Warning] $($_.Exception.Message)\" }\r\n                $QUserErrors = $Null\r\n            }\r\n\r\n            $i = 0\r\n            $CurrentlyLoggedInUsers = $QuserOutput | Where-Object { $_.Trim() } | ForEach-Object {\r\n                # Skip the first line (header) and process only the data lines\r\n                if ($i -ne 0 -and $_.Length -ge 65) {\r\n                    # Extract the relevant columns using fixed-width positions\r\n                    [PSCustomObject]@{\r\n                        Username    = ($_.Substring(0, 21).Trim() -replace '^&gt;')\r\n                        SessionName = $_.Substring(21, 21).Trim()\r\n                        ID          = $_.Substring(40, 5).Trim()\r\n                        State       = $_.Substring(45, 10).Trim()\r\n                        IdleTime    = $_.Substring(55, 10).Trim()\r\n                        LogonTime   = $_.Substring(65).Trim()\r\n                    }\r\n                }\r\n\r\n                $i++\r\n            }\r\n        } catch {\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Unable to check for currently logged-in users.\"\r\n            exit 1\r\n        }\r\n    }\r\n\r\n    if ($DisplayUserMessage -and $CurrentlyLoggedInUsers) {\r\n        $CurrentlyLoggedInUsers = $Null\r\n\r\n        # Define arguments for the 'msg.exe' command to display a system message to the user.\r\n        $MSGArguments = @(\r\n            \"*\"\r\n            \"\/TIME:$TotalMessageTime\"\r\n            \"\/V\"\r\n            \"System performance metrics are currently being collected. Collection should complete in approximately $TotalCollectionTime minutes and the results will be sent to your IT Administrator. Please do not restart the computer until this collection has completed.\"\r\n        )\r\n\r\n        # Attempt to display the system message to all users.\r\n        try {\r\n            Write-Host -Object \"Sending message to all users.\"\r\n\r\n            # Start the 'msg.exe' process with the arguments defined above.\r\n            Invoke-LegacyConsoleTool -FilePath \"$env:WINDIR\\System32\\msg.exe\" -ArgumentList $MSGArguments -ErrorAction Stop\r\n        } catch {\r\n            # If the 'msg.exe' process fails to start, output an error message and exit with a failure code.\r\n            Write-Host -Object \"[Error] Failed to send message to all users.\"\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            exit 1\r\n        }\r\n\r\n        # Output the exit code of the 'msg.exe' process.\r\n        Write-Host -Object \"ExitCode: $LASTEXITCODE\"\r\n\r\n        # If the exit code is non-zero (indicating an error occurred), display an error message.\r\n        if ($LASTEXITCODE -ne 0) {\r\n            Write-Host -Object \"[Error] ExitCode does not indicate success.\"\r\n            exit 1\r\n        }\r\n    }\r\n\r\n    # Get the last reboot time of the system.\r\n    try {\r\n        $LastStartTime = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop | Select-Object -ExpandProperty LastBootUpTime\r\n    } catch {\r\n        Write-Host -Object \"[Error] Failed to get last start up time.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        $ExitCode = 1\r\n    }\r\n\r\n    # If the 'DaysSinceLastReboot' parameter is set, calculate the time difference since the last reboot.\r\n    if ($DaysSinceLastReboot -ge 0) {\r\n        $TimeDifference = New-TimeSpan -Start $LastStartTime -End (Get-Date)\r\n\r\n        # If the time since the last reboot exceeds the limit, display an alert to the user.\r\n        if ($TimeDifference.TotalDays -gt $DaysSinceLastReboot) {\r\n            Write-Host -Object \"[Alert] This computer was last started on $($LastStartTime.ToShortDateString()) at $($LastStartTime.ToShortTimeString()) which was $([math]::Round($TimeDifference.TotalDays,2)) days ago.\"\r\n            $ExceededLastStartupLimit = $True\r\n        }\r\n    }\r\n\r\n    # Initialize an empty list to store event logs.\r\n    $EventLogs = New-Object System.Collections.Generic.List[object]\r\n\r\n    # Define XML queries for Application, Security, Setup, and System event logs that have error level events (Level=2).\r\n    [xml]$ApplicationXML = @\"\r\n&lt;QueryList&gt;\r\n  &lt;Query Id=\"0\" Path=\"Application\"&gt;\r\n    &lt;Select Path=\"Application\"&gt;*[System[(Level=2)]]&lt;\/Select&gt;\r\n  &lt;\/Query&gt;\r\n&lt;\/QueryList&gt;\r\n\"@\r\n\r\n    [xml]$SecurityLogs = @\"\r\n&lt;QueryList&gt;\r\n  &lt;Query Id=\"0\" Path=\"Application\"&gt;\r\n    &lt;Select Path=\"Security\"&gt;*[System[(Level=2)]]&lt;\/Select&gt;\r\n  &lt;\/Query&gt;\r\n&lt;\/QueryList&gt;\r\n\"@\r\n\r\n    [xml]$SetupLogs = @\"\r\n&lt;QueryList&gt;\r\n  &lt;Query Id=\"0\" Path=\"Application\"&gt;\r\n    &lt;Select Path=\"Setup\"&gt;*[System[(Level=2)]]&lt;\/Select&gt;\r\n  &lt;\/Query&gt;\r\n&lt;\/QueryList&gt;\r\n\"@\r\n\r\n    [xml]$SystemLogs = @\"\r\n&lt;QueryList&gt;\r\n  &lt;Query Id=\"0\" Path=\"Application\"&gt;\r\n    &lt;Select Path=\"System\"&gt;*[System[(Level=2)]]&lt;\/Select&gt;\r\n  &lt;\/Query&gt;\r\n&lt;\/QueryList&gt;\r\n\"@\r\n\r\n    # If the 'NumberOfEvents' parameter is set, collect the specified number of error logs from each log category.\r\n    if ($NumberOfEvents) {\r\n        Write-Host -Object \"`nCollecting event logs.\"\r\n\r\n        # Collect logs from each category and store them in the EventLogs list.\r\n        Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $ApplicationXML -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) }\r\n        Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SecurityLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) }\r\n        Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SetupLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) }\r\n        Get-WinEvent -MaxEvents $NumberOfEvents -FilterXml $SystemLogs -ErrorAction SilentlyContinue -ErrorVariable EventLogErrors | ForEach-Object { $EventLogs.Add($_) }\r\n\r\n        # If any errors occurred during log collection, display warnings with the error details.\r\n        if ($EventLogErrors) {\r\n            $EventLogErrors | ForEach-Object {\r\n                Write-Host -Object \"[Warning] $($_.Exception.Message)\"\r\n            }\r\n        }\r\n\r\n        # If no error logs were found, display a warning message.\r\n        if ($EventLogs.Count -eq 0) {\r\n            Write-Host -Object \"[Warning] No error events were found in the event log.\"\r\n        } else {\r\n            $EventLogs = $EventLogs | Select-Object LogName, ProviderName, Id, TimeCreated, Message | Sort-Object -Property TimeCreated -Descending\r\n        }\r\n    }\r\n\r\n    # Display a message to the user indicating the start of the search for performance counter localizations.\r\n    Write-Host -Object \"Searching for performance counter localizations.\"\r\n\r\n    # Attempt to retrieve the \"Counter\" property from the CurrentLanguage registry key, which contains the localized performance counter names.\r\n    # If the retrieval fails, catch the error, display an error message, and exit the script.\r\n    try {\r\n        $CurrentLanguageKey = Get-ItemProperty -Path \"Registry::HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Perflib\\CurrentLanguage\" -Name \"Counter\" -ErrorAction Stop | Select-Object -ExpandProperty Counter\r\n    } catch {\r\n        Write-Host -Object \"[Error] Failed to retrieve performance counter localizations.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        exit 1\r\n    }\r\n\r\n    # Initialize an empty hash table to store the performance counter localizations.\r\n    $LocalizationCounterTable = @{}\r\n\r\n    # Loop through the array of performance counters in the registry.\r\n    # The counter array consists of alternating key-value pairs (even indexes are keys, odd indexes are values),\r\n    # so this loop increments by 2 to match each key with its corresponding value.\r\n    for ($i = 0; $i -lt $CurrentLanguageKey.Length; $i += 2) {\r\n        $LocalizationCounterTable[$CurrentLanguageKey[$i]] = $CurrentLanguageKey[$i + 1]\r\n    }\r\n\r\n    # Define the paths for various performance counters using the localized counter names from the hash table.\r\n    # These paths are dynamically created by retrieving the localized names for each counter ID.\r\n    $OverallProcessCounterPath = \"\\$($LocalizationCounterTable['238'])(*)\\$($LocalizationCounterTable['6'])\"\r\n    $OverallMemoryCounterPath = \"\\$($LocalizationCounterTable['4'])\\$($LocalizationCounterTable['1406'])\"\r\n    $ProcessorCounterPath = \"\\$($LocalizationCounterTable['230'])(*)\\$($LocalizationCounterTable['142'])\"\r\n    $MemoryCounterPath = \"\\$($LocalizationCounterTable['230'])(*)\\$($LocalizationCounterTable['1478'])\"\r\n    $IOUsageCounterPath = \"\\$($LocalizationCounterTable['230'])(*)\\$($LocalizationCounterTable['1424'])\"\r\n    $DiskUsageCounterPath = \"\\$($LocalizationCounterTable['234'])(*)\\$($LocalizationCounterTable['212'])\"\r\n    $NetworkUsageCounterPath = \"\\$($LocalizationCounterTable['510'])(*)\\$($LocalizationCounterTable['388'])\"\r\n\r\n    # Notify the user that performance metrics are being collected for the specified duration.\r\n    Write-Host -Object \"Collecting performance metrics for $DurationToPerformTests minutes.\"\r\n\r\n    # Collect performance metrics (CPU, memory, disk, and network usage) at a 60-second interval for the specified duration.\r\n    $PerformanceMetrics = Get-Counter -MaxSamples $DurationToPerformTests -SampleInterval 60 -Counter $OverallProcessCounterPath, $OverallMemoryCounterPath,\r\n    $ProcessorCounterPath, $MemoryCounterPath, $IOUsageCounterPath, $DiskUsageCounterPath, $NetworkUsageCounterPath -ErrorAction SilentlyContinue -ErrorVariable PerformanceMetricErrors\r\n\r\n    # Extract performance metrics for CPU, memory, I\/O, disk, and network usage from the collected data.\r\n    $OverallProcessorUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match \"$([Regex]::Escape($($LocalizationCounterTable['6'])))$\" }\r\n    $OverallMemoryUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match \"$([Regex]::Escape($($LocalizationCounterTable['1406'])))$\" }\r\n    $ProcessorUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match \"$([Regex]::Escape($($LocalizationCounterTable['142'])))$\" }\r\n    $MemoryUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match \"$([Regex]::Escape($($LocalizationCounterTable['1478'])))$\" }\r\n    $IOUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match \"$([Regex]::Escape($($LocalizationCounterTable['1424'])))$\" }\r\n    $DiskUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match \"$([Regex]::Escape($($LocalizationCounterTable['212'])))$\" }\r\n    $NetworkUsage = $PerformanceMetrics | Select-Object -ExpandProperty CounterSamples | Where-Object { $_.Path -match \"$([Regex]::Escape($($LocalizationCounterTable['388'])))$\" }\r\n\r\n    # If there were errors during the collection of performance metrics, display a warning message for each error.\r\n    if ($PerformanceMetricErrors) {\r\n        $PerformanceMetricErrors | ForEach-Object {\r\n            Write-Host -Object \"[Warning] $($_.Exception.Message)\"\r\n        }\r\n    }\r\n\r\n    # Ensure that performance metrics for CPU, memory, I\/O, disk, and network usage were successfully retrieved.\r\n    # If any of the metrics are missing, display an error message and exit the script.\r\n    if (!$OverallProcessorUsage -or !$OverallMemoryUsage -or !$ProcessorUsage -or !$MemoryUsage -or !$IOUsage -or !$DiskUsage -or !$NetworkUsage) {\r\n        Write-Host -Object \"[Error] Failed to retrieve performance metrics.\"\r\n        exit 1\r\n    }\r\n\r\n    # Retrieve CPU information such as name and clock speed (in GHz).\r\n    try {\r\n        $CPU = \"$(Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -ExpandProperty Name) $((Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -ExpandProperty MaxClockSpeed)\/1000) GHz\"\r\n\r\n        # Retrieve the total amount of installed physical memory (RAM) in bytes and convert it to GB.\r\n        $TotalMemoryBytes = Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction Stop | Measure-Object -Property Capacity -Sum | Select-Object -ExpandProperty Sum\r\n        $TotalMemoryGB = \"$($TotalMemoryBytes\/1GB) GB\"\r\n    } catch {\r\n        Write-Host -Object \"[Error] Unable to get CPU or Memory details.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        $ExitCode = 1\r\n    }\r\n\r\n    # Display the CPU information.\r\n    Write-Host -Object \"`n### $CPU ###\"\r\n\r\n    # Filter and sort the relevant CPU performance metrics for the \"_total\" instance (overall system usage).\r\n    $RelevantMetrics = $OverallProcessorUsage | Where-Object { $_.InstanceName -eq \"_total\" } | Sort-Object CookedValue\r\n\r\n    # Calculate average, minimum, and maximum CPU usage.\r\n    $CPUPerformance = [PSCustomObject]@{\r\n        Avg = [math]::Round((($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) \/ $DurationToPerformTests), 2)\r\n        Min = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1), 2)\r\n        Max = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1), 2)\r\n    }\r\n\r\n    # Format the CPU performance metrics for display.\r\n    $FormattedCPUPerformance = [PSCustomObject]@{\r\n        \"CPU Average %\" = \"$($CPUPerformance.Avg)%\"\r\n        \"CPU Minimum %\" = \"$($CPUPerformance.Min)%\"\r\n        \"CPU Maximum %\" = \"$($CPUPerformance.Max)%\"\r\n    }\r\n\r\n    # Display the formatted CPU performance metrics.\r\n    ($FormattedCPUPerformance | Format-Table -AutoSize | Out-String).Trim() | Write-Host\r\n\r\n    # Display memory usage header.\r\n    Write-Host -Object \"`n### Memory Usage ###\"\r\n    Write-Host -Object \"Total Memory Installed: $TotalMemoryGB\"\r\n\r\n    # Filter and sort the relevant memory usage metrics.\r\n    $RelevantMetrics = $OverallMemoryUsage | Sort-Object CookedValue\r\n\r\n    # Calculate average, minimum, and maximum memory usage.\r\n    $MemoryPerformance = [PSCustomObject]@{\r\n        Avg = [math]::Round((($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) \/ $DurationToPerformTests), 2)\r\n        Min = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1), 2)\r\n        Max = [math]::Round(($RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1), 2)\r\n    }\r\n\r\n    # Format the memory performance metrics for display.\r\n    $OverallMemoryMetrics = [PSCustomObject]@{\r\n        \"RAM Average %\" = \"$($MemoryPerformance.Avg)%\"\r\n        \"RAM Minimum %\" = \"$($MemoryPerformance.Min)%\"\r\n        \"RAM Maximum %\" = \"$($MemoryPerformance.Max)%\"\r\n    }\r\n\r\n    # Display the formatted memory performance metrics.\r\n    ($OverallMemoryMetrics | Format-Table -AutoSize | Out-String).Trim() | Write-Host\r\n\r\n    # Display the header for the top 5 CPU processes.\r\n    Write-Host \"`n### Top 5 CPU Processes ###\"\r\n\r\n    # Get a unique list of all process names excluding the \"_total\" instance.\r\n    $AllProcessNames = $ProcessorUsage | Where-Object { $_.InstanceName -ne \"_total\" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName\r\n\r\n    # Initialize an empty list to store process metrics.\r\n    $Processes = New-Object -TypeName System.Collections.Generic.List[object]\r\n\r\n    # Loop through each process name to calculate the CPU usage (min, max, avg) for each process.\r\n    foreach ($ProcessName in $AllProcessNames) {\r\n        $RelevantMetrics = $ProcessorUsage | Where-Object { $_.InstanceName -eq $ProcessName }\r\n\r\n        # Group metrics by timestamp and calculate the total CPU usage for each timestamp.\r\n        $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = \"InstanceName\"; Expression = { $ProcessName } }, @{Name = \"CookedValue\"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue\r\n\r\n        # Add the CPU usage metrics (min, max, avg) for each process to the list.\r\n        $Processes.Add(\r\n            [PSCustomObject]@{\r\n                \"InstanceName\" = $ProcessName\r\n                \"Min\"          = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1\r\n                \"Max\"          = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1\r\n                \"Avg\"          = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) \/ $DurationToPerformTests\r\n            }\r\n        )\r\n    }\r\n\r\n    # Sort the processes by average CPU usage in descending order and select the top 5.\r\n    $Top5CPUProcesses = $Processes | Sort-Object \"Avg\" -Descending | Select-Object -First 5\r\n\r\n    # Format the top 5 CPU processes for display.\r\n    $FormattedProcesses = $Top5CPUProcesses | ForEach-Object {\r\n        [PSCustomObject]@{\r\n            \"Process Name\"       = $_.InstanceName\r\n            \"Average CPU % Used\" = \"$([math]::Round($_.Avg, 2))%\"\r\n            \"Minimum CPU % Used\" = \"$([math]::Round($_.Min, 2))%\"\r\n            \"Maximum CPU % Used\" = \"$([math]::Round($_.Max, 2))%\"\r\n        }\r\n    }\r\n\r\n    # Display the formatted CPU process usage metrics.\r\n    ($FormattedProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host\r\n\r\n    # Display the header for the top 5 RAM processes.\r\n    Write-Host -Object \"`n### Top 5 RAM Processes ###\"\r\n\r\n    # Get a unique list of process names that are not \"_total\" or \"memory compression\".\r\n    $AllMemoryProcessNames = $MemoryUsage | Where-Object { $_.InstanceName -ne \"_total\" -and $_.InstanceName -ne \"memory compression\" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName\r\n\r\n    # Initialize an empty list to store memory process metrics.\r\n    $MemoryProcesses = New-Object -TypeName System.Collections.Generic.List[object]\r\n\r\n    # Loop through each process to calculate the memory usage (min, max, avg) for each process.\r\n    foreach ($ProcessName in $AllMemoryProcessNames) {\r\n        $RelevantMetrics = $MemoryUsage | Where-Object { $_.InstanceName -eq $ProcessName }\r\n\r\n        # Group metrics by timestamp and calculate the total memory usage for each timestamp.\r\n        $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = \"InstanceName\"; Expression = { $ProcessName } }, @{Name = \"CookedValue\"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue\r\n\r\n        # Add the memory usage metrics (min, max, avg) for each process to the list.\r\n        $MemoryProcesses.Add(\r\n            [PSCustomObject]@{\r\n                \"InstanceName\" = $ProcessName\r\n                \"Min\"          = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1\r\n                \"Max\"          = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1\r\n                \"Avg\"          = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) \/ $DurationToPerformTests\r\n            }\r\n        )\r\n    }\r\n\r\n    # Sort the processes by average memory usage in descending order and select the top 5.\r\n    $Top5RAMProcesses = $MemoryProcesses | Sort-Object \"Avg\" -Descending | Select-Object -First 5 | ForEach-Object {\r\n        if (!$TotalMemoryBytes) {\r\n            return\r\n        }\r\n\r\n        [PSCustomObject]@{\r\n            \"InstanceName\" = $_.InstanceName\r\n            \"Min\"          = $_.Min \/ $TotalMemoryBytes * 100\r\n            \"Max\"          = $_.Max \/ $TotalMemoryBytes * 100\r\n            \"Avg\"          = $_.Avg \/ $TotalMemoryBytes * 100\r\n        }\r\n    }\r\n\r\n    # Format the top 5 RAM processes for display.\r\n    $FormattedMemoryProcesses = $Top5RAMProcesses | ForEach-Object {\r\n        if (!$TotalMemoryBytes) {\r\n            return\r\n        }\r\n\r\n        [PSCustomObject]@{\r\n            \"Process Name\"       = $_.InstanceName\r\n            \"Average RAM % Used\" = \"$([math]::Round($_.Avg, 2))%\"\r\n            \"Minimum RAM % Used\" = \"$([math]::Round($_.Min, 2))%\"\r\n            \"Maximum RAM % Used\" = \"$([math]::Round($_.Max, 2))%\"\r\n        }\r\n    }\r\n\r\n    # Display the formatted memory process usage metrics.\r\n    ($FormattedMemoryProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host\r\n\r\n    # Display the header for network usage.\r\n    Write-Host -Object \"`n### Network Usage ###\"\r\n\r\n    # Get a unique list of network interfaces and initialize an empty list for storing network metrics.\r\n    $NetworkInterfaces = $NetworkUsage | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName\r\n    $NetworkInterfaceUsage = New-Object -TypeName System.Collections.Generic.List[object]\r\n\r\n    # Loop through each network interface to calculate the network usage (min, max, avg) for each interface.\r\n    foreach ($NetworkInterface in $NetworkInterfaces) {\r\n        $RelevantMetrics = $NetworkUsage | Where-Object { $_.InstanceName -eq $NetworkInterface } | Sort-Object CookedValue\r\n\r\n        try {\r\n            # Correct the network interface name if necessary to match the system's adapter description.\r\n            if (!(Get-NetAdapter -ErrorAction Stop | Where-Object { $_.InterfaceDescription -eq $NetworkInterface })) {\r\n                $NetworkInterface = $NetworkInterface -replace '\\[', '(' -replace '\\]', ')'\r\n            }\r\n\r\n            # Retrieve the network adapter details and determine if it's wired, Wi-Fi, or another type.\r\n            $NetAdapter = Get-NetAdapter -ErrorAction Stop | Where-Object { $_.InterfaceDescription -eq $NetworkInterface } | Select-Object -First 1\r\n            if (!$NetAdapter.MacAddress) {\r\n                continue\r\n            }\r\n\r\n            switch -Wildcard ($NetAdapter.MediaType) {\r\n                \"802.3\" { $AdapterType = \"Wired\" }\r\n                \"*802.11\" { $AdapterType = \"Wi-Fi\" }\r\n                default { $AdapterType = \"Other\" }\r\n            }\r\n        } catch {\r\n            Write-Host -Object \"[Error] Failed to get details on the network interface '$NetworkInterface'.\"\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)`n\"\r\n            $ExitCode = 1\r\n            continue\r\n        }\r\n\r\n        # Add the network adapter usage metrics to the list.\r\n        $NetworkInterfaceUsage.Add(\r\n            [PSCustomObject]@{\r\n                \"NetworkAdapter\" = $NetworkInterface\r\n                \"MacAddress\"     = $NetAdapter.MacAddress\r\n                \"Type\"           = $AdapterType\r\n                \"Min\"            = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1\r\n                \"Max\"            = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1\r\n                \"Avg\"            = ($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) \/ $DurationToPerformTests\r\n            }\r\n        )\r\n    }\r\n\r\n    # Format the network usage metrics for display.\r\n    $FormattedNetworkUsage = $NetworkInterfaceUsage | Sort-Object \"Avg\" -Descending | ForEach-Object {\r\n        [PSCustomObject]@{\r\n            \"NetworkAdapter\"          = $_.NetworkAdapter\r\n            \"MacAddress\"              = $_.MacAddress\r\n            \"Type\"                    = $_.Type\r\n            \"Average Sent &amp; Received\" = \"$([math]::Round(($_.Avg \/ 1MB * 8), 2)) Mbps\"\r\n            \"Minimum Sent &amp; Received\" = \"$([math]::Round(($_.Min \/ 1MB * 8), 2)) Mbps\"\r\n            \"Maximum Sent &amp; Received\" = \"$([math]::Round(($_.Max \/ 1MB * 8), 2)) Mbps\"\r\n        }\r\n    }\r\n\r\n    # Display the formatted network usage metrics.\r\n    ($FormattedNetworkUsage | Format-List | Out-String).Trim() | Write-Host\r\n\r\n    # Display the header for disk usage.\r\n    Write-Host -Object \"`n### Disk Usage ###\"\r\n\r\n    # Get a unique list of relevant disks and initialize an empty list for storing disk metrics.\r\n    $RelevantDisks = $DiskUsage | Where-Object { $_.InstanceName -ne \"_total\" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName\r\n    $DiskMetrics = New-Object -TypeName System.Collections.Generic.List[object]\r\n\r\n    try {\r\n        $AllDiskNumbers = Get-Partition -ErrorAction Stop | Select-Object -ExpandProperty DiskNumber -Unique\r\n    } catch {\r\n        Write-Host -Object \"[Error] Unable to retrieve disk numbers.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        $ExitCode = 1\r\n    }\r\n\r\n    # Loop through each disk to calculate the disk usage (min, max, avg) for each disk.\r\n    foreach ($RelevantDisk in $RelevantDisks) {\r\n        $RelevantMetrics = $DiskUsage | Where-Object { $_.InstanceName -eq $RelevantDisk } | Sort-Object CookedValue\r\n\r\n        # Parse the disk number and drive letter from the instance name.\r\n        $DiskNumber = $RelevantDisk -split '\\s' | Where-Object { $_ -match \"^[0-9]$\" }\r\n        $DriveLetters = ($RelevantDisk -split '\\s' | Where-Object { $_ -match \"^[A-z]:$\" }) -replace ':'\r\n\r\n        # Retrieve the physical disk based on the provided DiskNumber.\r\n        $PhysicalDisk = Get-PhysicalDisk -ErrorAction SilentlyContinue | Where-Object { $_.DeviceId -eq $DiskNumber }\r\n\r\n        # Check if the disk number is part of the list of all disk numbers.\r\n        if ($AllDiskNumbers -and $AllDiskNumbers -notcontains $DiskNumber) {\r\n\r\n            # If the physical disk has a FriendlyName (meaning it was found), warn that no partitions were found on this disk.\r\n            if ($PhysicalDisk.FriendlyName) {\r\n                Write-Host -Object \"[Warning] No partitions found on disk '$($PhysicalDisk.FriendlyName)'.\"\r\n            } else {\r\n                # If the physical disk has no FriendlyName, display a warning message using the DiskNumber.\r\n                Write-Host -Object \"[Warning] No partitions found on disk '$DiskNumber'.\"\r\n            }\r\n\r\n            Write-Host -Object \"\"\r\n\r\n            # Continue to the next iteration in the loop, skipping the remaining code for this disk number.\r\n            continue\r\n        }\r\n\r\n        # Attempt to retrieve the partitions for the specified disk number.\r\n        try {\r\n            $Partitions = Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop\r\n        } catch {\r\n            # If an error occurs while getting the partitions, display an error message.\r\n            Write-Host -Object \"[Error] Accessing Partitions on disk '$DiskNumber'\"\r\n\r\n            # Display the exception message from the caught error.\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)`n\"\r\n\r\n            # Set the exit code to indicate an error occurred.\r\n            $ExitCode = 1\r\n\r\n            # Continue to the next iteration in the loop, skipping further actions for this disk number.\r\n            continue\r\n        }\r\n\r\n        # Retrieve partition information and add the disk usage metrics to the list.\r\n        foreach ($DriveLetter in $DriveLetters) {\r\n            $Partitions | Where-Object { $_.DriveLetter -eq $DriveLetter } | ForEach-Object {\r\n                try {\r\n                    $FreeSpace = Get-Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq $DriveLetter } | Select-Object -ExpandProperty SizeRemaining\r\n                    $TotalSize = Get-Volume -ErrorAction Stop | Where-Object { $_.DriveLetter -eq $DriveLetter } | Select-Object -ExpandProperty Size\r\n                } catch {\r\n                    Write-Host -Object \"[Error] Unable to determine the total size or free space of drive '$DriveLetter'.\"\r\n                    Write-Host -Object \"[Error] $($_.Exception.Message)`n\"\r\n                    $ExitCode = 1\r\n                    continue\r\n                }\r\n\r\n                $FreeSpaceGB = [math]::Round(($FreeSpace \/ 1GB), 2)\r\n                $FreeSpacePercent = [math]::Round(($FreeSpace \/ $TotalSize * 100), 2)\r\n                $TotalSpaceGB = [math]::Round(($TotalSize \/ 1GB), 2)\r\n\r\n                # Add the disk metrics to the list.\r\n                $DiskMetrics.Add(\r\n                    [PSCustomObject]@{\r\n                        \"DriveLetter\"      = $_.DriveLetter\r\n                        \"FreeSpaceGB\"      = $FreeSpaceGB\r\n                        \"FreeSpacePercent\" = $FreeSpacePercent\r\n                        \"TotalSpace\"       = \"$TotalSpaceGB GB\"\r\n                        \"PhysicalDisk\"     = $PhysicalDisk | Select-Object -ExpandProperty FriendlyName\r\n                        \"MediaType\"        = $PhysicalDisk | Select-Object -ExpandProperty MediaType\r\n                        \"Min\"              = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -First 1\r\n                        \"Max\"              = $RelevantMetrics | Select-Object -ExpandProperty CookedValue -Last 1\r\n                        \"Avg\"              = ($RelevantMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) \/ $DurationToPerformTests\r\n                    }\r\n                )\r\n            }\r\n        }\r\n    }\r\n\r\n    # Add the disk metrics to the list.\r\n    $FormattedDiskMetrics = $DiskMetrics | Sort-Object \"Avg\" -Descending | ForEach-Object {\r\n        [PSCustomObject]@{\r\n            \"DriveLetter\"  = $_.DriveLetter\r\n            \"FreeSpace\"    = \"$($_.FreeSpaceGB) GB ($($_.FreeSpacePercent)%)\"\r\n            \"TotalSpace\"   = $_.TotalSpace\r\n            \"PhysicalDisk\" = $_.PhysicalDisk\r\n            \"MediaType\"    = $_.MediaType\r\n            \"Average IOPS\" = \"$([math]::Round(($_.Avg), 2)) IOPS\"\r\n            \"Minimum IOPS\" = \"$([math]::Round(($_.Min), 2)) IOPS\"\r\n            \"Maximum IOPS\" = \"$([math]::Round(($_.Max), 2)) IOPS\"\r\n        }\r\n    }\r\n\r\n    # Display the formatted disk usage metrics.\r\n    ($FormattedDiskMetrics | Format-List | Out-String).Trim() | Write-Host\r\n\r\n    # Display the header for top 5 I\/O processes (network and disk combined).\r\n    Write-Host -Object \"`n### Top 5 IO Processes (Network &amp; Disk Combined) ###\"\r\n\r\n    # Get a unique list of I\/O process names excluding the \"_total\" instance.\r\n    $AllIOProcessNames = $IOUsage | Where-Object { $_.InstanceName -ne \"_total\" } | Sort-Object InstanceName -Unique | Select-Object -ExpandProperty InstanceName\r\n    $IOProcesses = New-Object -TypeName System.Collections.Generic.List[object]\r\n\r\n    # Loop through each process to calculate the I\/O usage (min, max, avg) for each process.\r\n    foreach ($ProcessName in $AllIOProcessNames) {\r\n        $RelevantMetrics = $IOUsage | Where-Object { $_.InstanceName -eq $ProcessName }\r\n\r\n        # Group metrics by timestamp and calculate the total I\/O usage for each timestamp.\r\n        $GroupedMetrics = $RelevantMetrics | Group-Object Timestamp | Select-Object @{Name = \"InstanceName\"; Expression = { $ProcessName } }, @{Name = \"CookedValue\"; Expression = { $_.Group | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum } } | Sort-Object CookedValue\r\n\r\n        # Add the I\/O usage metrics to the list.\r\n        $IOProcesses.Add(\r\n            [PSCustomObject]@{\r\n                \"InstanceName\" = $ProcessName\r\n                \"Min\"          = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -First 1\r\n                \"Max\"          = $GroupedMetrics | Select-Object -ExpandProperty CookedValue -Last 1\r\n                \"Avg\"          = ($GroupedMetrics | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum) \/ $DurationToPerformTests\r\n            }\r\n        )\r\n    }\r\n\r\n    # Sort the I\/O processes by average I\/O usage and select the top 5.\r\n    $Top5IOProcesses = $IOProcesses | Sort-Object \"Avg\" -Descending | Select-Object -First 5\r\n\r\n    # Format the top 5 I\/O processes for display.\r\n    $FormattedIOProcesses = $Top5IOProcesses | ForEach-Object {\r\n        [PSCustomObject]@{\r\n            \"Process Name\"    = $_.InstanceName\r\n            \"Average IO Used\" = \"$([math]::Round(($_.Avg \/ 1MB * 8), 4)) Mbps\"\r\n            \"Minimum IO Used\" = \"$([math]::Round(($_.Min \/ 1MB * 8), 4)) Mbps\"\r\n            \"Maximum IO Used\" = \"$([math]::Round(($_.Max \/ 1MB * 8), 4)) Mbps\"\r\n        }\r\n    }\r\n\r\n    # Display the formatted I\/O process usage metrics.\r\n    ($FormattedIOProcesses | Format-Table -AutoSize | Out-String).Trim() | Write-Host\r\n\r\n    # Inform the user that WinSAT assessments are running.\r\n    Write-Host -Object \"`nRetrieving WinSAT assessment data.\"\r\n    Write-Host -Object \"More info: https:\/\/learn.microsoft.com\/en-us\/previous-versions\/windows\/it-pro\/windows-8.1-and-8\/hh825488(v=win.10)\"\r\n\r\n    # Retrieve the WinSAT assessment scores.\r\n    try {\r\n        $WinSatScores = Get-CimInstance -ClassName Win32_WinSAT -ErrorAction Stop\r\n    } catch {\r\n        Write-Host -Object \"[Error] Unable to retrieve WinSat assessment results.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        $ExitCode = 1\r\n    }\r\n\r\n    # Handle the different possible states of the WinSAT assessment.\r\n    switch ($WinSatScores.WinSATAssessmentState) {\r\n        0 { Write-Host -Object \"[Error] WinSAT assessment data is not available on this computer. Is this computer a virtual machine?\" ; $ExitCode = 1 }\r\n        1 { Write-Host -Object \"Successfully retrieved assessment data.\" }\r\n        2 { Write-Host -Object \"[Warning] The WinSAT assessment data does not match the current computer configuration.\" }\r\n        3 { Write-Host -Object \"[Error] WinSAT assessment data is not available on this computer. Is this computer a virtual machine?\" ; $ExitCode = 1 }\r\n        4 { Write-Host -Object \"[Error] The WinSAT assessment data is not valid!\" ; $ExitCode = 1 }\r\n        default {\r\n            Write-Host -Object \"[Error] WinSAT assessment data is not available on this computer. Is this computer a virtual machine?\" ; $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    # If the WinSAT assessment state is valid, display the assessment scores.\r\n    $ValidAssessmentStates = \"1\", \"2\"\r\n    if ($ValidAssessmentStates -contains $WinSatScores.WinSATAssessmentState) {\r\n        Write-Host -Object \"`n### WinSAT Scores ###\"\r\n        ($WinSatScores | Format-Table -Property CPUScore, D3DScore, DiskScore, GraphicsScore, MemoryScore | Out-String).Trim() | Write-Host\r\n    }\r\n\r\n    # If the WYSIWYG custom field is given, proceed to set and format the custom field.\r\n    if ($WysiwygCustomField) {\r\n        try {\r\n            # Inform the user that the custom field is being set.\r\n            Write-Host \"`nAttempting to set Custom Field '$WysiwygCustomField'.\"\r\n\r\n            $CompletedDateTime = Get-Date\r\n\r\n            # Initialize the custom field value as a list of strings.\r\n            $CustomFieldValue = New-Object System.Collections.Generic.List[String]\r\n            $CustomFieldValue.Add(\"&lt;div&gt;\")\r\n\r\n            # Convert the formatted CPU processes table to HTML and add custom formatting.\r\n            $CPUProcessMetricTable = $FormattedProcesses | ConvertTo-Html -Fragment\r\n            $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;th&gt;\", \"&lt;th&gt;&lt;b&gt;\" -replace \"&lt;\/th&gt;\", \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n            $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;table&gt;\", \"&lt;table&gt;&lt;caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'&gt;&lt;b&gt;Top 5 CPU Processes&lt;\/b&gt;&lt;\/caption&gt;\"\r\n            $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"Average CPU % Used\", \"&lt;i class='fa-solid fa-arrow-down-up-across-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Average CPU % Used\"\r\n            $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"Minimum CPU % Used\", \"&lt;i class='fa-solid fa-arrows-down-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Minimum CPU % Used\"\r\n            $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"Maximum CPU % Used\", \"&lt;i class='fa-solid fa-arrows-up-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Maximum CPU % Used\"\r\n\r\n            # Highlight rows in the CPU table based on CPU usage thresholds (warnings and danger levels).\r\n            $Top5CPUProcesses | ForEach-Object {\r\n                if ($_.Avg -ge 20 -and $_.Avg -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Min -ge 20 -and $_.Min -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Max -ge 20 -and $_.Max -lt 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n\r\n                if ($_.Avg -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Min -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Max -ge 50) { $CPUProcessMetricTable = $CPUProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n            }\r\n\r\n            # Convert the formatted RAM processes table to HTML and add custom formatting.\r\n            $RAMProcessMetricTable = $FormattedMemoryProcesses | ConvertTo-Html -Fragment\r\n            $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;th&gt;\", \"&lt;th&gt;&lt;b&gt;\" -replace \"&lt;\/th&gt;\", \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n            $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;table&gt;\", \"&lt;table&gt;&lt;caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'&gt;&lt;b&gt;Top 5 RAM Processes&lt;\/b&gt;&lt;\/caption&gt;\"\r\n            $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"Average RAM % Used\", \"&lt;i class='fa-solid fa-arrow-down-up-across-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Average RAM % Used\"\r\n            $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"Minimum RAM % Used\", \"&lt;i class='fa-solid fa-arrows-down-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Minimum RAM % Used\"\r\n            $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"Maximum RAM % Used\", \"&lt;i class='fa-solid fa-arrows-up-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Maximum RAM % Used\"\r\n\r\n            # Highlight rows in the RAM table based on RAM usage thresholds (warnings and danger levels).\r\n            $Top5RAMProcesses | ForEach-Object {\r\n                if ($_.Avg -ge 10 -and $_.Avg -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Min -ge 10 -and $_.Min -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Max -ge 10 -and $_.Max -lt 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n\r\n                if ($_.Avg -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Min -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Max -ge 30) { $RAMProcessMetricTable = $RAMProcessMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n            }\r\n\r\n            # Convert the formatted I\/O processes table to HTML and add custom formatting.\r\n            $IOProcessesMetricTable = $FormattedIOProcesses | ConvertTo-Html -Fragment\r\n            $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;th&gt;\", \"&lt;th&gt;&lt;b&gt;\" -replace \"&lt;\/th&gt;\", \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n            $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;table&gt;\", \"&lt;table&gt;&lt;caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'&gt;&lt;b&gt;Top 5 IO Processes (Network &amp; Disk Combined)&lt;\/b&gt;&lt;\/caption&gt;\"\r\n            $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"Average IO Used\", \"&lt;i class='fa-solid fa-arrow-down-up-across-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Average IO Used\"\r\n            $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"Minimum IO Used\", \"&lt;i class='fa-solid fa-arrows-down-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Minimum IO Used\"\r\n            $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"Maximum IO Used\", \"&lt;i class='fa-solid fa-arrows-up-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Maximum IO Used\"\r\n\r\n            # Highlight rows in the I\/O table based on I\/O usage thresholds (warnings and danger levels).\r\n            $Top5IOProcesses | ForEach-Object {\r\n                if ($_.Avg -ge 1250000 -and $_.Avg -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Min -ge 1250000 -and $_.Min -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Max -ge 1250000 -and $_.Max -lt 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n\r\n                if ($_.Avg -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Min -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n                if ($_.Max -ge 12500000) { $IOProcessesMetricTable = $IOProcessesMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.InstanceName)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.InstanceName)\" }\r\n            }\r\n\r\n            # Convert the formatted network usage table to HTML and add custom formatting.\r\n            $NetworkUsageMetricTable = $FormattedNetworkUsage | ConvertTo-Html -Fragment\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;th&gt;\", \"&lt;th&gt;&lt;b&gt;\" -replace \"&lt;\/th&gt;\", \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;table&gt;\", \"&lt;table&gt;&lt;caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'&gt;&lt;b&gt;Network Usage&lt;\/b&gt;&lt;\/caption&gt;\"\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"Average Sent &amp; Received\", \"&lt;i class='fa-solid fa-arrow-down-up-across-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Average Sent &amp; Received\"\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"Minimum Sent &amp; Received\", \"&lt;i class='fa-solid fa-arrows-down-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Minimum Sent &amp; Received\"\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"Maximum Sent &amp; Received\", \"&lt;i class='fa-solid fa-arrows-up-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Maximum Sent &amp; Received\"\r\n\r\n            # Add network type icons for wired, Wi-Fi, and other network interfaces.\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;th&gt;&lt;b&gt;Type&lt;\/b&gt;&lt;\/th&gt;\", \"&lt;th&gt;&lt;b&gt;&lt;i class='fa-solid fa-network-wired'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Type&lt;\/b&gt;&lt;\/th&gt;\"\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;td&gt;Wired&lt;\/td&gt;\", \"&lt;td&gt;&lt;i class='fa-solid fa-ethernet'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Wired&lt;\/td&gt;\"\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;td&gt;Wi-Fi&lt;\/td&gt;\", \"&lt;td&gt;&lt;i class='fa-solid fa-wifi'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Wi-Fi&lt;\/td&gt;\"\r\n            $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;td&gt;Other&lt;\/td&gt;\", \"&lt;td&gt;&lt;i class='fa-solid fa-circle-question'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Other&lt;\/td&gt;\"\r\n\r\n            # Highlight network interfaces based on network usage thresholds and interface types.\r\n            $NetworkInterfaceUsage | ForEach-Object {\r\n                if ($_.Avg -ge 1250000 -and $_.Avg -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.NetworkAdapter)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.NetworkAdapter)\" }\r\n                if ($_.Min -ge 1250000 -and $_.Min -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.NetworkAdapter)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.NetworkAdapter)\" }\r\n                if ($_.Max -ge 1250000 -and $_.Max -lt 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.NetworkAdapter)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.NetworkAdapter)\" }\r\n\r\n                if ($_.Avg -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.NetworkAdapter)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.NetworkAdapter)\" }\r\n                if ($_.Min -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.NetworkAdapter)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.NetworkAdapter)\" }\r\n                if ($_.Max -ge 12500000) { $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.NetworkAdapter)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.NetworkAdapter)\" }\r\n\r\n                # Highlight Wi-Fi or \"Other\" types as warnings.\r\n                if ($_.Type -eq \"Wi-Fi\" -or $_.Type -eq \"Other\") {\r\n                    $NetworkUsageMetricTable = $NetworkUsageMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.NetworkAdapter)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.NetworkAdapter)\"\r\n                }\r\n            }\r\n\r\n            # Convert the formatted disk usage table to HTML and add custom formatting.\r\n            $DiskMetricTable = $FormattedDiskMetrics | ConvertTo-Html -Fragment\r\n            $DiskMetricTable = $DiskMetricTable -replace \"&lt;th&gt;\", \"&lt;th&gt;&lt;b&gt;\" -replace \"&lt;\/th&gt;\", \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n            $DiskMetricTable = $DiskMetricTable -replace \"&lt;table&gt;\", \"&lt;table&gt;&lt;caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'&gt;&lt;b&gt;Disk Usage&lt;\/b&gt;&lt;\/caption&gt;\"\r\n            $DiskMetricTable = $DiskMetricTable -replace \"Average IOPS\", \"&lt;i class='fa-solid fa-arrow-down-up-across-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Average IOPS\"\r\n            $DiskMetricTable = $DiskMetricTable -replace \"Minimum IOPS\", \"&lt;i class='fa-solid fa-arrows-down-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Minimum IOPS\"\r\n            $DiskMetricTable = $DiskMetricTable -replace \"Maximum IOPS\", \"&lt;i class='fa-solid fa-arrows-up-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Maximum IOPS\"\r\n\r\n            # Highlight rows in the disk usage table based on drive type and available space thresholds.\r\n            $DiskMetrics | ForEach-Object {\r\n                if ($_.MediaType -ne \"SSD\" -and $_.MediaType -ne \"Unspecified\") {\r\n                    $DiskMetricTable = $DiskMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.DriveLetter)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.DriveLetter)\"\r\n                }\r\n\r\n                if ($_.FreeSpaceGB -lt 100) {\r\n                    $DiskMetricTable = $DiskMetricTable -replace \"&lt;tr&gt;&lt;td&gt;$($_.DriveLetter)\", \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.DriveLetter)\"\r\n                }\r\n\r\n                if ($_.FreeSpaceGB -lt 10) {\r\n                    $DiskMetricTable = $DiskMetricTable -replace \"&lt;tr class='warning'&gt;&lt;td&gt;$($_.DriveLetter)\", \"&lt;tr class='danger'&gt;&lt;td&gt;$($_.DriveLetter)\"\r\n                }\r\n            }\r\n\r\n            # Handle WinSAT assessment data if it's valid and add the WinSAT scores to the table.\r\n            $ValidAssessmentStates = \"1\", \"2\"\r\n            if ($ValidAssessmentStates -contains $WinSatScores.WinSATAssessmentState) {\r\n                $WinSATMetricTable = $WinSatScores | Select-Object -Property CPUScore, D3DScore, DiskScore, GraphicsScore, MemoryScore | ConvertTo-Html -Fragment\r\n                $WinSATMetricTable = $WinSATMetricTable -replace \"&lt;th&gt;\", \"&lt;th&gt;&lt;b&gt;\" -replace \"&lt;\/th&gt;\", \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n                $WinSATMetricTable = $WinSATMetricTable -replace \"&lt;table&gt;\", \"&lt;br&gt;&lt;table&gt;&lt;caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'&gt;&lt;b&gt;WinSAT Scores&lt;\/b&gt;&lt;\/caption&gt;\"\r\n\r\n                # Highlight rows in the WinSAT table based on score thresholds.\r\n                if ($WinSatScores.CPUScore -lt 7 -or $WinSatScores.D3DScore -lt 7 -or $WinSatScores.DiskScore -lt 7 -or $WinSatScores.GraphicsScore -lt 7 -or $WinSatScores.MemoryScore -lt 7) {\r\n                    $WinSATMetricTable = $WinSATMetricTable -replace \"&lt;tr&gt;&lt;td&gt;\", \"&lt;tr class='warning'&gt;&lt;td&gt;\"\r\n                }\r\n\r\n                if ($WinSatScores.CPUScore -lt 4 -or $WinSatScores.D3DScore -lt 4 -or $WinSatScores.DiskScore -lt 4 -or $WinSatScores.GraphicsScore -lt 4 -or $WinSatScores.MemoryScore -lt 4) {\r\n                    $WinSATMetricTable = $WinSATMetricTable -replace \"&lt;tr class='warning'&gt;&lt;td&gt;\", \"&lt;tr class='danger'&gt;&lt;td&gt;\"\r\n                }\r\n            } else {\r\n                # If WinSAT data is not available, display a message.\r\n                $WinSATMetricTable = \"&lt;p style='margin-top: 0px'&gt;The WinSAT assessment data is either invalid or not available for this computer.&lt;\/p&gt;\"\r\n            }\r\n\r\n            # Create the HTML content for the performance metrics section.\r\n            $HTMLCard = \"&lt;div class='card flex-grow-1'&gt;\r\n    &lt;div class='card-title-box'&gt;\r\n        &lt;div class='card-title'&gt;&lt;i class='fa-solid fa-gauge-high'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;System Performance Metrics&lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n    &lt;div class='card-body' style='white-space: nowrap'&gt;\r\n        &lt;div class='container' style='padding-left: 0px'&gt;\r\n            &lt;div class='row'&gt;\r\n                &lt;div class='col-sm'&gt;\r\n                    &lt;p class='card-text'&gt;&lt;b&gt;Start Date and Time&lt;\/b&gt;&lt;br&gt;$($StartedDateTime.ToShortDateString()) $($StartedDateTime.ToShortTimeString())&lt;\/p&gt;\r\n                &lt;\/div&gt;\r\n                &lt;div class='col-sm'&gt;\r\n                    &lt;p class='card-text'&gt;&lt;b&gt;Completed Date and Time&lt;\/b&gt;&lt;br&gt;$($CompletedDateTime.ToShortDateString()) $($CompletedDateTime.ToShortTimeString())&lt;\/p&gt;\r\n                &lt;\/div&gt;\r\n            &lt;\/div&gt;\r\n        &lt;\/div&gt;\r\n        &lt;p id='lastStartup' class='card-text'&gt;&lt;b&gt;Last Startup Time&lt;\/b&gt;&lt;br&gt;$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())&lt;\/p&gt;\r\n        &lt;div class='container my-4' style='padding-left: 0px; margin-left: 0px; padding-right: 0px; margin-right: 0px;'&gt;\r\n            &lt;div class='row' style='padding-left: 0px'&gt;\r\n                &lt;p&gt;&lt;b&gt;$CPU&lt;\/b&gt;&lt;\/p&gt;\r\n            &lt;\/div&gt;\r\n            &lt;div class='row' style='padding-left: 0px'&gt;\r\n                &lt;div class='col-sm-4'&gt;\r\n                    &lt;div class='stat-card' style='display: flex;'&gt;\r\n                        &lt;div class='stat-value' id='cpuOverallAvg' style='color: #008001;'&gt;$($FormattedCPUPerformance.\"CPU Average %\")&lt;\/div&gt;\r\n                        &lt;div class='stat-desc'&gt;&lt;i class='fa-solid fa-arrow-down-up-across-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Average CPU % Used&lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n                &lt;div class='col-sm-4'&gt;\r\n                    &lt;div class='stat-card' style='display: flex;'&gt;\r\n                        &lt;div class='stat-value' id='cpuOverallMin' style='color: #008001;'&gt;$($FormattedCPUPerformance.\"CPU Minimum %\")&lt;\/div&gt;\r\n                        &lt;div class='stat-desc'&gt;&lt;i class='fa-solid fa-arrows-down-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Minimum CPU % Used&lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n                &lt;div class='col-sm-4'&gt;\r\n                    &lt;div class='stat-card' style='display: flex;'&gt;\r\n                        &lt;div class='stat-value' id='cpuOverallMax' style='color: #008001;'&gt;$($FormattedCPUPerformance.\"CPU Maximum %\")&lt;\/div&gt;\r\n                        &lt;div class='stat-desc'&gt;&lt;i class='fa-solid fa-arrows-up-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Maximum CPU % Used&lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n            &lt;\/div&gt;\r\n            &lt;div class='row' style='padding-left: 0px'&gt;\r\n                &lt;p&gt;&lt;b&gt;Total Memory: $TotalMemoryGB&lt;\/b&gt;&lt;\/p&gt;\r\n            &lt;\/div&gt;\r\n            &lt;div class='row' style='padding-left: 0px'&gt;\r\n                &lt;div class='col-sm-4'&gt;\r\n                    &lt;div class='stat-card' style='display: flex;'&gt;\r\n                        &lt;div class='stat-value' id='ramOverallAvg' style='color: #008001;'&gt;$($OverallMemoryMetrics.\"RAM Average %\")&lt;\/div&gt;\r\n                        &lt;div class='stat-desc'&gt;&lt;i class='fa-solid fa-arrow-down-up-across-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Average RAM % Used&lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n                &lt;div class='col-sm-4'&gt;\r\n                    &lt;div class='stat-card' style='display: flex;'&gt;\r\n                        &lt;div class='stat-value' id='ramOverallMin' style='color: #008001;'&gt;$($OverallMemoryMetrics.\"RAM Minimum %\")&lt;\/div&gt;\r\n                        &lt;div class='stat-desc'&gt;&lt;i class='fa-solid fa-arrows-down-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Minimum RAM % Used&lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n                &lt;div class='col-sm-4'&gt;\r\n                    &lt;div class='stat-card' style='display: flex;'&gt;\r\n                        &lt;div class='stat-value' id='ramOverallMax' style='color: #008001;'&gt;$($OverallMemoryMetrics.\"RAM Maximum %\")&lt;\/div&gt;\r\n                        &lt;div class='stat-desc'&gt;&lt;i class='fa-solid fa-arrows-up-to-line'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Maximum RAM % Used&lt;\/div&gt;\r\n                    &lt;\/div&gt;\r\n                &lt;\/div&gt;\r\n            &lt;\/div&gt;\r\n        &lt;\/div&gt;\r\n        $CPUProcessMetricTable\r\n        &lt;br&gt;\r\n        $RAMProcessMetricTable\r\n        &lt;br&gt;\r\n        $NetworkUsageMetricTable\r\n        &lt;br&gt;\r\n        $DiskMetricTable\r\n        &lt;br&gt;\r\n        $IOProcessesMetricTable\r\n        $(if($ValidAssessmentStates -notcontains $WinSatScores.WinSATAssessmentState) {\"&lt;p style='margin-bottom: 0px'&gt;&lt;b&gt;WinSAT Scores&lt;\/b&gt;&lt;\/p&gt;\"})\r\n        $WinSATMetricTable\r\n    &lt;\/div&gt;\r\n&lt;\/div&gt;\"\r\n            # Modify the last startup time section based on whether the startup limit was exceeded or not.\r\n            if ($ExceededLastStartupLimit) {\r\n                $HTMLCard = $HTMLCard -replace \"id='lastStartup' class='card-text'&gt;&lt;b&gt;Last Startup Time&lt;\/b&gt;&lt;br&gt;$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())\", \"id='lastStartup' class='card-text'&gt;&lt;b&gt;Last Startup Time&lt;\/b&gt;&lt;br&gt;$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())&amp;nbsp;&amp;nbsp;&lt;i class='fa-solid fa-circle-exclamation' style='color: #D53948;'&gt;&lt;\/i&gt;\"\r\n            } elseif ($DaysSinceLastReboot -ge 0) {\r\n                $HTMLCard = $HTMLCard -replace \"id='lastStartup' class='card-text'&gt;&lt;b&gt;Last Startup Time&lt;\/b&gt;&lt;br&gt;$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())\", \"id='lastStartup' class='card-text'&gt;&lt;b&gt;Last Startup Time&lt;\/b&gt;&lt;br&gt;$($LastStartTime.ToShortDateString()) $($LastStartTime.ToShortTimeString())&amp;nbsp;&amp;nbsp;&lt;i class='fa-solid fa-circle-check' style='color: #008001;'&gt;&lt;\/i&gt;\"\r\n            }\r\n\r\n            # Highlight CPU performance metrics based on threshold values (color coding).\r\n            if ($CPUPerformance.Avg -ge 60 -and $CPUPerformance.Avg -lt 90) { $HTMLCard = $HTMLCard -replace \"id='cpuOverallAvg' style='color: #008001;'\", \"id='cpuOverallAvg' style='color: #FAC905;'\" }\r\n            if ($CPUPerformance.Min -ge 60 -and $CPUPerformance.Min -lt 90) { $HTMLCard = $HTMLCard -replace \"id='cpuOverallMin' style='color: #008001;'\", \"id='cpuOverallMin' style='color: #FAC905;'\" }\r\n            if ($CPUPerformance.Max -ge 60 -and $CPUPerformance.Max -lt 90) { $HTMLCard = $HTMLCard -replace \"id='cpuOverallMax' style='color: #008001;'\", \"id='cpuOverallMax' style='color: #FAC905;'\" }\r\n\r\n            if ($CPUPerformance.Avg -ge 90) { $HTMLCard = $HTMLCard -replace \"id='cpuOverallAvg' style='color: #008001;'\", \"id='cpuOverallAvg' style='color: #D53948;'\" }\r\n            if ($CPUPerformance.Min -ge 90) { $HTMLCard = $HTMLCard -replace \"id='cpuOverallMin' style='color: #008001;'\", \"id='cpuOverallMin' style='color: #D53948;'\" }\r\n            if ($CPUPerformance.Max -ge 90) { $HTMLCard = $HTMLCard -replace \"id='cpuOverallMax' style='color: #008001;'\", \"id='cpuOverallMax' style='color: #D53948;'\" }\r\n\r\n            # Highlight RAM performance metrics based on threshold values (color coding).\r\n            if ($MemoryPerformance.Avg -ge 60 -and $MemoryPerformance.Avg -lt 90) { $HTMLCard = $HTMLCard -replace \"id='ramOverallAvg' style='color: #008001;'\", \"id='ramOverallAvg' style='color: #FAC905;'\" }\r\n            if ($MemoryPerformance.Min -ge 60 -and $MemoryPerformance.Min -lt 90) { $HTMLCard = $HTMLCard -replace \"id='ramOverallMin' style='color: #008001;'\", \"id='ramOverallMin' style='color: #FAC905;'\" }\r\n            if ($MemoryPerformance.Max -ge 60 -and $MemoryPerformance.Max -lt 90) { $HTMLCard = $HTMLCard -replace \"id='ramOverallMax' style='color: #008001;'\", \"id='ramOverallMax' style='color: #FAC905;'\" }\r\n\r\n            if ($MemoryPerformance.Avg -ge 90) { $HTMLCard = $HTMLCard -replace \"id='ramOverallAvg' style='color: #008001;'\", \"id='ramOverallAvg' style='color: #D53948;'\" }\r\n            if ($MemoryPerformance.Min -ge 90) { $HTMLCard = $HTMLCard -replace \"id='ramOverallMin' style='color: #008001;'\", \"id='ramOverallMin' style='color: #D53948;'\" }\r\n            if ($MemoryPerformance.Max -ge 90) { $HTMLCard = $HTMLCard -replace \"id='ramOverallMax' style='color: #008001;'\", \"id='ramOverallMax' style='color: #D53948;'\" }\r\n\r\n            # Add the created HTML card to the custom field.\r\n            $CustomFieldValue.Add($HTMLCard)\r\n\r\n            # Check if there are any event logs to display.\r\n            if ($NumberOfEvents -gt 0 -and $EventLogs.Count -gt 0) {\r\n                # Convert the event logs into an HTML fragment for displaying in the output.\r\n                $EventLogTableMetrics = $EventLogs | ConvertTo-Html -Fragment\r\n\r\n                # Apply custom styles to the HTML table headers.\r\n                $EventLogTableMetrics = $EventLogTableMetrics -replace \"&lt;th&gt;\", \"&lt;th&gt;&lt;b&gt;\" -replace \"&lt;\/th&gt;\", \"&lt;\/b&gt;&lt;\/th&gt;\"\r\n\r\n                # Set specific column widths for better presentation.\r\n                $EventLogTableMetrics = $EventLogTableMetrics -replace \"&lt;th&gt;&lt;b&gt;LogName\", \"&lt;th style='width: 100px'&gt;&lt;b&gt;Log Name\"\r\n                $EventLogTableMetrics = $EventLogTableMetrics -replace \"&lt;th&gt;&lt;b&gt;ProviderName\", \"&lt;th style='width: 250px'&gt;&lt;b&gt;Provider Name\"\r\n                $EventLogTableMetrics = $EventLogTableMetrics -replace \"&lt;th&gt;&lt;b&gt;Id\", \"&lt;th style='width: 75px'&gt;&lt;b&gt;Id\"\r\n                $EventLogTableMetrics = $EventLogTableMetrics -replace \"&lt;th&gt;&lt;b&gt;TimeCreated\", \"&lt;th style='width: 175px'&gt;&lt;b&gt;Time Created\"\r\n            } elseif ($NumberOfEvents -gt 0) {\r\n                # If no events were found, display a message instead of the table.\r\n                $EventLogTableMetrics = \"&lt;p style='margin-top: 0px'&gt;No error events were found in the event log.&lt;\/p&gt;\"\r\n            }\r\n\r\n            # If event logs exist, create a card to display them.\r\n            if ($NumberOfEvents -gt 0) {\r\n                # Create the HTML structure for the event log card.\r\n                $EventLogCard = \"&lt;div class='card flex-grow-1'&gt;\r\n    &lt;div class='card-title-box'&gt;\r\n        &lt;div class='card-title'&gt;&lt;i class='fa-solid fa-book'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Recent Error Events&lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n    &lt;div class='card-body' style='white-space: nowrap'&gt;\r\n        $EventLogTableMetrics\r\n    &lt;\/div&gt;\r\n&lt;\/div&gt;\"\r\n                # Add the event log card to the custom field value.\r\n                $CustomFieldValue.Add($EventLogCard)\r\n            }\r\n\r\n            # Check if the HTML content exceeds the character limit (45,000 characters).\r\n            $HTMLCharacters = ($CustomFieldValue | Out-String).Length\r\n            if ($HTMLCharacters -ge 190000) {\r\n                Write-Host -Object \"The current character count is '$HTMLCharacters'.\"\r\n                Write-Host -Object \"[Warning] The character limit of 200,000 has been reached! Trimming the output until the character limit is satisfied.\"\r\n\r\n                # Truncate the output if it exceeds the limit.\r\n                $i = 0\r\n                [array]$NewEventLogTable = $EventLogTableMetrics\r\n                $TrimStart = Get-Date\r\n                do {\r\n                    # Recreate the custom field output\r\n                    $CustomFieldValue = New-Object System.Collections.Generic.List[string]\r\n                    $CustomFieldValue.Add(\"&lt;div&gt;\")\r\n\r\n                    if (!$NumberOfEvents -or !$NumberOfEvents -gt 0 -or !$EventLogs.Count -gt 0) {\r\n                        Write-Host -Object \"[Error] No events to trim.\"\r\n                        exit 1\r\n                    }\r\n\r\n                    # Add the main performance metrics card to the custom field.\r\n                    $CustomFieldValue.Add($HTMLCard)\r\n\r\n                    # Calculate the batch size for trimming\r\n                    $ExceededAmount = $HTMLCharacters - 190000\r\n                    if ($ExceededAmount -le 0) {\r\n                        $BatchSize = 1\r\n                    } else {\r\n                        $BatchSize = [math]::Ceiling($ExceededAmount \/ 500)\r\n                    }\r\n\r\n                    # Reverse the event log array so that the last entry is at the top.\r\n                    [array]::Reverse($NewEventLogTable)\r\n\r\n                    # Remove excess event log entries\r\n                    for ($BatchNumber = 0 ; $BatchNumber -lt $BatchSize ; $BatchNumber++) {\r\n                        # Delete rows until the character count is reduced.\r\n                        if ($NewEventLogTable[$i] -match '&lt;tr&gt;&lt;td&gt;' -or $NewEventLogTable[$i] -match '&lt;tr class=') {\r\n                            $NewEventLogTable[$i] = $null\r\n                        }\r\n                        $i++\r\n                    }\r\n\r\n                    # Reverse the array back to its original order.\r\n                    [array]::Reverse($NewEventLogTable)\r\n\r\n                    # Rebuild the event log card with the truncated log.\r\n                    $EventLogCard = \"&lt;div class='card flex-grow-1'&gt;\r\n    &lt;div class='card-title-box'&gt;\r\n        &lt;div class='card-title'&gt;&lt;i class='fa-solid fa-book'&gt;&lt;\/i&gt;&amp;nbsp;&amp;nbsp;Recent Error Events&lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n    &lt;div class='card-body' style='white-space: nowrap'&gt;\r\n        $NewEventLogTable\r\n    &lt;\/div&gt;\r\n&lt;\/div&gt;\"\r\n\r\n                    # Add a truncation notice and the truncated event log card.\r\n                    $CustomFieldValue.Add(\"&lt;h1&gt;This information has been truncated to fit within the character limit of 200,000.&lt;\/h1&gt;\")\r\n                    $CustomFieldValue.Add($EventLogCard)\r\n\r\n                    # Check the character count again; repeat if still too long.\r\n                    $HTMLCharacters = ($CustomFieldValue | Out-String).Length\r\n                    $ElapsedTime = (Get-Date) - $TrimStart\r\n                    if ($ElapsedTime.TotalMinutes -ge 5) {\r\n                        Write-Host -Object \"The current character count is '$HTMLCharacters'.\"\r\n                        throw \"5 minute timeout reached. Unable to trim the output to comply with the character limit.\"\r\n                    }\r\n                }while ($HTMLCharacters -ge 190000)\r\n            }\r\n\r\n            $CustomFieldValue.Add(\"&lt;\/div&gt;\")\r\n\r\n            # Set the custom field with the finalized HTML content.\r\n            Set-NinjaProperty -Name $WysiwygCustomField -Value $CustomFieldValue -Type \"WYSIWYG\" -ErrorAction Stop\r\n            Write-Host \"Successfully set Custom Field '$WysiwygCustomField'!\"\r\n        } catch {\r\n            Write-Host \"[Error] $($_.Exception.Message)\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    # Try to remove the lock file to ensure no other instance of the script is running.\r\n    try {\r\n        Remove-Item -Path \"$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt\" -Force -ErrorAction Stop\r\n    } catch {\r\n        # If the removal of the lock file fails, catch the exception and display error messages.\r\n        Write-Host -Object \"[Error] Failed to remove lock file at '$env:ProgramData\\NinjaRMMAgent\\SystemPerformance.lock.txt'.\"\r\n        Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n        $ExitCode = 1\r\n    }\r\n\r\n    if ($DisplayUserMessage) {\r\n        try {\r\n            Write-Host -Object \"`nChecking for currently logged-in users.\"\r\n            $QuserOutput = Invoke-LegacyConsoleTool -FilePath \"$env:WINDIR\\System32\\quser.exe\" -ErrorVariable \"QUserErrors\" -ErrorAction SilentlyContinue\r\n            if ($QUserErrors) {\r\n                $QUserErrors | ForEach-Object { Write-Host -Object \"[Warning] $($_.Exception.Message)\" }\r\n                $QUserErrors = $Null\r\n            }\r\n\r\n            $i = 0\r\n            $CurrentlyLoggedInUsers = $QuserOutput | Where-Object { $_.Trim() } | ForEach-Object {\r\n                # Skip the first line (header) and process only the data lines\r\n                if ($i -ne 0 -and $_.Length -ge 65) {\r\n                    # Extract the relevant columns using fixed-width positions\r\n                    [PSCustomObject]@{\r\n                        Username    = ($_.Substring(0, 21).Trim() -replace '^&gt;')\r\n                        SessionName = $_.Substring(21, 21).Trim()\r\n                        ID          = $_.Substring(40, 5).Trim()\r\n                        State       = $_.Substring(45, 10).Trim()\r\n                        IdleTime    = $_.Substring(55, 10).Trim()\r\n                        LogonTime   = $_.Substring(65).Trim()\r\n                    }\r\n                }\r\n\r\n                $i++\r\n            }\r\n        } catch {\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Unable to check for currently logged-in users.\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    if ($DisplayUserMessage -and $CurrentlyLoggedInUsers) {\r\n        $CurrentlyLoggedInUsers = $null\r\n\r\n        # Arguments for sending a message to notify the user that performance metrics have been recorded.\r\n        $MSGArguments = @(\r\n            \"*\"\r\n            \"\/TIME:3600\"\r\n            \"\/V\"\r\n            \"Performance metrics have been recorded and forwarded to your IT Administrator.\"\r\n        )\r\n\r\n        # Display an empty line for better readability in the output.\r\n        Write-Host -Object \"\"\r\n\r\n        # Start the process of sending a message to the users using msg.exe.\r\n        try {\r\n            Write-Host -Object \"Sending message to all users.\"\r\n\r\n            # Start the 'msg.exe' process with the provided arguments and capture stdout and stderr into log files.\r\n            Invoke-LegacyConsoleTool -FilePath \"$env:WINDIR\\System32\\msg.exe\" -ArgumentList $MSGArguments -ErrorAction Stop\r\n        } catch {\r\n            # If the process fails to start, output an error message and exit the script with an error code.\r\n            Write-Host -Object \"[Error] Failed to send message to all users.\"\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            $ExitCode = 1\r\n        }\r\n\r\n        # Output the exit code of the msg.exe process.\r\n        Write-Host -Object \"ExitCode: $LASTEXITCODE\"\r\n\r\n        # If the exit code is non-zero (indicating an error), display an error message.\r\n        if ($LASTEXITCODE -ne 0) {\r\n            Write-Host -Object \"[Error] ExitCode does not indicate success.\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    Write-Host -Object \"\"\r\n\r\n    # Iterate through each feedback log file location\r\n    foreach ($LogFile in $FeedbackLogLocations) {\r\n        Write-Host -Object \"Updating the feedback log at '$($LogFile.LogFileLocation)'.\"\r\n\r\n        try {\r\n            # Attempt to read the first line of the log file to ensure it exists\r\n            Get-Content -Path $LogFile.LogFileLocation -TotalCount 1 -ErrorAction Stop | Out-Null\r\n        } catch {\r\n            # Log an error if unable to read the log file\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to retrieve the feedback log file contents for the user '$($LogFile.Name)'.\"\r\n            $ExitCode = 1\r\n            continue\r\n        }\r\n\r\n        # Initialize variables for parsing and updating the log file\r\n        $i = 0\r\n        $UpdateLogFile = $False\r\n\r\n        # Read and process the log file content line by line\r\n        $LogContent = Get-Content -Path $($LogFile.LogFileLocation) | ForEach-Object {\r\n            $i++\r\n            # Split each line into data points using the '|' delimiter\r\n            $DataPoints = $_ -split '\\|', 6\r\n\r\n            # Warn if a line has fewer than 6 data points\r\n            if ($DataPoints.Count -lt 6) {\r\n                Write-Host -Object \"[Warning] Only $($DataPoints.Count) data points were found for line '$i'.\"\r\n                $_\r\n                return\r\n            }\r\n\r\n            # If the fifth data point is already True, skip updating this line\r\n            if ($DataPoints[4] -eq $True) {\r\n                $_\r\n                return\r\n            }\r\n\r\n            # Replace the fifth data point with True and mark the file for update\r\n            $_ -replace [regex]::Escape(\"$($DataPoints[4])|$($DataPoints[5])\"), \"$True|$($DataPoints[5])\"\r\n            $UpdateLogFile = $True\r\n        }\r\n\r\n        # If no updates are needed, skip to the next log file\r\n        if (!$UpdateLogFile) {\r\n            Write-Host -Object \"'$($LogFile.LogFileLocation)' is already up-to-date.\"\r\n            continue\r\n        }\r\n\r\n        try {\r\n            # Write the updated content back to the log file\r\n            Set-Content -Path $LogFile.LogFileLocation -Value $LogContent -ErrorAction Stop\r\n            Write-Host -Object \"Updated the log file at '$($LogFile.LogFileLocation)'.\"\r\n        } catch {\r\n            # Log an error if unable to update the log file\r\n            Write-Host -Object \"[Error] $($_.Exception.Message)\"\r\n            Write-Host -Object \"[Error] Failed to update the log file at '$($LogFile.LogFileLocation)'.\"\r\n            $ExitCode = 1\r\n        }\r\n    }\r\n\r\n    # If the $NumberOfEvents variable has a value, proceed to display the event logs.\r\n    if ($NumberOfEvents) {\r\n        # Display a message indicating the number of errors retrieved from the event logs.\r\n        Write-Host -Object \"`n### Last $NumberOfEvents errors in Application, Security, Setup and System Log. ###\"\r\n\r\n        # Format and display the collected event logs in a list format.\r\n        ($EventLogs | Format-List | Out-String).Trim() | Write-Host\r\n    }\r\n\r\n    exit $ExitCode\r\n}\r\nend {\r\n    \r\n    \r\n    \r\n}<\/pre>\n<p>&nbsp;<\/p>\n\n<h2>Description d\u00e9taill\u00e9e<\/h2>\n<p>Le script effectue les op\u00e9rations suivantes :<\/p>\n<ol>\n<li><strong>Validation de l&rsquo;environnement et des donn\u00e9es<\/strong><strong><br \/>\n<\/strong>Il valide les param\u00e8tres d&rsquo;entr\u00e9e tels que<\/p>\n<ul>\n<li><strong><code>DaysSinceLastReboot<\/code><\/strong><\/li>\n<li><strong><code>DurationToPerformTests<\/code><\/strong><\/li>\n<li><strong><code>NumberOfEvents<\/code><\/strong><\/li>\n<li><strong><code>WysiwygCustomField<\/code><\/strong><\/li>\n<\/ul>\n<\/li>\n<li>Il garantit une \u00e9l\u00e9vation correcte des privil\u00e8ges, la compatibilit\u00e9 avec le syst\u00e8me d&rsquo;exploitation et emp\u00eache l&rsquo;ex\u00e9cution simultan\u00e9e de scripts \u00e0 l&rsquo;aide d&rsquo;un fichier de verrouillage.<\/li>\n<li><strong>Logique de d\u00e9clenchement de la r\u00e9troaction<\/strong><strong><br \/>\n<\/strong>Il lit les fichiers journaux de r\u00e9troaction de plusieurs profils d&rsquo;utilisateurs, \u00e0 la recherche d&rsquo;entr\u00e9es non r\u00e9solues qui justifient une analyse des performances.<\/li>\n<li><strong>Messagerie utilisateur (optionnel)<\/strong><strong><br \/>\n<\/strong>Si <strong><code>-DisplayUserMessage<\/code><\/strong> est activ\u00e9, il envoie des notifications \u00e0 l&rsquo;\u00e9chelle de la session pour avertir les utilisateurs de ne pas red\u00e9marrer pendant que les mesures sont collect\u00e9es.<\/li>\n<li><strong>Collecte de donn\u00e9es<\/strong><strong><br \/>\n<\/strong>Pendant la dur\u00e9e d\u00e9finie (par d\u00e9faut : 5 minutes), il utilise la fonction <strong><code>Get-Counter<\/code><\/strong> et des requ\u00eates WMI\/CIM pour collecter les donn\u00e9es :<\/p>\n<ul>\n<li>Utilisation de l&rsquo;unit\u00e9 centrale (total et 5 premiers processus)<\/li>\n<li>Consommation de m\u00e9moire vive<\/li>\n<li>IOPS du disque et espace disponible<\/li>\n<li>D\u00e9bit r\u00e9seau par adaptateur<\/li>\n<li>Principaux processus par E\/S<\/li>\n<li>R\u00e9sultats au WinSAT (si disponibles)<\/li>\n<\/ul>\n<\/li>\n<li><strong>G\u00e9n\u00e9ration de rapports HTML<\/strong><strong><br \/>\n<\/strong>Il construit dynamiquement un rapport HTML stylis\u00e9 avec des graphiques, des ic\u00f4nes et des alertes cod\u00e9es en couleur. Les indicateurs cl\u00e9s sont class\u00e9s dans des cartes de style Bootstrap.<\/li>\n<li><strong>Stockage de champs personnalis\u00e9s<\/strong><strong><br \/>\n<\/strong>Si un nom de champ WYSIWYG est fourni, le rapport est \u00e9crit dans le stockage de champ personnalis\u00e9 de NinjaOne \u00e0 l&rsquo;aide de la fonction <strong><code>Set-NinjaProperty<\/code><\/strong>.<\/li>\n<li><strong>Mise \u00e0 jour du journal des avis<\/strong><strong><br \/>\n<\/strong>Une fois les donn\u00e9es de performance recueillies, la ligne correspondante dans le journal des avis est marqu\u00e9e comme compl\u00e9t\u00e9e.<\/li>\n<\/ol>\n<h2>Cas d&rsquo;utilisation potentiels<\/h2>\n<h3>Cas de figure\u00a0: Un employ\u00e9 \u00e0 distance signale des lenteurs dans le syst\u00e8me<\/h3>\n<p>Un technicien MSP ex\u00e9cute le script 1 de l&rsquo;enqu\u00eate DEX et re\u00e7oit un indicateur de satisfaction n\u00e9gatif. Le script 3 se d\u00e9clenche automatiquement en fonction de ce retour d&rsquo;information. La sortie montre une utilisation \u00e9lev\u00e9e du processeur par tiworker.exe et un <a href=\"https:\/\/www.ninjaone.com\/fr\/script-hub\/comment-verifier-espace-disque-script-powershell\/\">faible espace disque disponible<\/a>. Sur la base de ces \u00e9l\u00e9ments, le technicien planifie la maintenance, en lib\u00e9rant de l&rsquo;espace et en corrigeant le comportement de Windows Update.<\/p>\n<p>En int\u00e9grant l&rsquo;instantan\u00e9 de performance dans le ticket via un champ personnalis\u00e9, les futurs \u00e9valuateurs peuvent facilement comparer la performance avant et apr\u00e8s la rem\u00e9diation.<\/p>\n<h2>Comparaisons<\/h2>\n<h3>Approche diagnostique traditionnelle<\/h3>\n<ul>\n<li>Se connecter manuellement \u00e0 la machine par RDP.<\/li>\n<li>Lancez le gestionnaire des t\u00e2ches ou le moniteur de ressources.<\/li>\n<li>Enregistrer les mesures dans un document Word ou un ticket.<\/li>\n<\/ul>\n<p><strong>Inconv\u00e9nients<\/strong>: Exigeant en main-d&rsquo;\u0153uvre, long \u00e0 impl\u00e9menter, manque de base historique.<\/p>\n<h3>Script PowerShell NinjaOne<\/h3>\n<ul>\n<li>Automatis\u00e9.<\/li>\n<li>D\u00e9clench\u00e9 par l&rsquo;utilisateur en fonction du contexte du retour d&rsquo;information.<\/li>\n<li>R\u00e9sultats int\u00e9gr\u00e9s dans le tableau de bord RMM.<\/li>\n<li>Le format HTML permet une mise en forme coh\u00e9rente, des alertes de couleur et des comparaisons.<\/li>\n<\/ul>\n<h2>Questions fr\u00e9quentes<\/h2>\n<h3>Q\u00a0: Le script n\u00e9cessite-t-il des droits d&rsquo;administrateur ?<\/h3>\n<p>Oui. Il v\u00e9rifie l&rsquo;\u00e9l\u00e9vation et se termine s&rsquo;il n&rsquo;est pas ex\u00e9cut\u00e9 avec les privil\u00e8ges de l&rsquo;administrateur.<\/p>\n<h3>Q\u00a0: Comment le rapport est-il stock\u00e9 ?<\/h3>\n<p>Si vous sp\u00e9cifiez <strong><code>-WysiwygCustomField<\/code><\/strong> le rapport est inject\u00e9 dans le champ WYSIWYG de NinjaOne avec du HTML stylis\u00e9 pour plus de lisibilit\u00e9.<\/p>\n<h3>Q\u00a0: Que se passe-t-il si aucun retour d&rsquo;information n&rsquo;indique la n\u00e9cessit\u00e9 d&rsquo;un contr\u00f4le des performances ?<\/h3>\n<p>Le script ignore les tests de performance et se termine proprement apr\u00e8s l&rsquo;enregistrement de la d\u00e9cision.<\/p>\n<h3>Q\u00a0: Quels sont les seuils d&rsquo;alerte ?<\/h3>\n<ul>\n<li>Avertissements CPU : &gt;60%, danger : &gt;90%<\/li>\n<li>Avertissements RAM : &gt;60%, danger : &gt;90%<\/li>\n<li>Les seuils IOPS et d&rsquo;utilisation du r\u00e9seau sont \u00e9chelonn\u00e9s en fonction du nombre d&rsquo;octets transf\u00e9r\u00e9s.<\/li>\n<\/ul>\n<h2>Implications<\/h2>\n<p>Ce script permet de boucler la boucle entre la surveillance de l&rsquo;exp\u00e9rience utilisateur et les performances r\u00e9elles du syst\u00e8me. Ses implications sont consid\u00e9rables :<\/p>\n<ul>\n<li>Permet de prendre des d\u00e9cisions d&rsquo;assistance fond\u00e9es sur des donn\u00e9es.<\/li>\n<li>Am\u00e9liore la transparence de la qualit\u00e9 des services.<\/li>\n<li>D\u00e9tecte les causes profondes qui risquent de passer inaper\u00e7ues lors d&rsquo;un triage superficiel.<\/li>\n<li>Aide les \u00e9quipes de s\u00e9curit\u00e9 \u00e0 d\u00e9tecter les anomalies telles que les entr\u00e9es\/sorties inhabituelles ou les processus malveillants.<\/li>\n<\/ul>\n<p>En capturant \u00e0 la fois des donn\u00e9es subjectives (feedback) et objectives (m\u00e9triques), il cr\u00e9e une piste d&rsquo;audit solide pour les diagnostics des terminaux.<\/p>\n<h2>Recommandations<\/h2>\n<ul>\n<li><strong>\u00c0 utiliser avec le script 1 et le script 2<\/strong> pour construire une boucle compl\u00e8te de retour d&rsquo;information et de r\u00e9ponse.<\/li>\n<li><strong>Programmez les ex\u00e9cutions en dehors des heures de travail<\/strong> ou pendant les p\u00e9riodes de faible activit\u00e9.<\/li>\n<li><strong>Marquez les champs personnalis\u00e9s<\/strong> avec des horodatages pour conserver le contexte historique.<\/li>\n<li><strong>Surveillez le comportement du fichier de verrouillage<\/strong> afin d&rsquo;\u00e9viter les probl\u00e8mes de concurrence sur les terminaux fr\u00e9quemment analys\u00e9s.<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>L&rsquo;int\u00e9gration des diagnostics de performance directement dans les flux de travail de l&rsquo;exp\u00e9rience utilisateur transforme les op\u00e9rations informatiques r\u00e9actives en un mod\u00e8le de service proactif. Avec NinjaOne et ce script PowerShell, les MSP et les services informatiques peuvent automatiquement d\u00e9tecter, documenter et r\u00e9pondre aux probl\u00e8mes des utilisateurs finaux en une seule exp\u00e9rience optimale. La sortie visuelle stock\u00e9e dans des champs WYSIWYG permet d&rsquo;\u00e9tablir des rapports \u00e9l\u00e9gants et d&rsquo;analyser rapidement les causes profondes, le tout sans avoir \u00e0 se connecter manuellement \u00e0 l&rsquo;appareil.<\/p>\n<p>Que vous g\u00e9riez un parc de 10 ou 10 000 appareils, ce script constitue une \u00e9tape importante vers une <a href=\"https:\/\/www.ninjaone.com\/fr\/plateforme-de-gestion-de-terminaux\/\">gestion des terminaux<\/a> plus intelligente et plus contextuelle.<\/p>\n","protected":false},"author":35,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"_relevanssi_hide_post":"","_relevanssi_hide_content":"","_relevanssi_pin_for_all":"","_relevanssi_pin_keywords":"","_relevanssi_unpin_keywords":"","_relevanssi_related_keywords":"","_relevanssi_related_include_ids":"","_relevanssi_related_exclude_ids":"","_relevanssi_related_no_append":"","_relevanssi_related_not_related":"","_relevanssi_related_posts":"","_relevanssi_noindex_reason":"","_lmt_disableupdate":"no","_lmt_disable":""},"operating_system":[4212],"use_cases":[4284],"class_list":["post-531425","script_hub","type-script_hub","status-publish","hentry","script_hub_category-windows"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/script_hub\/531425","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/script_hub"}],"about":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/types\/script_hub"}],"author":[{"embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/users\/35"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/comments?post=531425"}],"wp:attachment":[{"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/media?parent=531425"}],"wp:term":[{"taxonomy":"script_hub_category","embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/operating_system?post=531425"},{"taxonomy":"use_cases","embeddable":true,"href":"https:\/\/www.ninjaone.com\/fr\/wp-json\/wp\/v2\/use_cases?post=531425"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}