diff --git a/docs/Tutorials/Watchdog/Overview.md b/docs/Tutorials/Watchdog/Overview.md new file mode 100644 index 000000000..02acaae06 --- /dev/null +++ b/docs/Tutorials/Watchdog/Overview.md @@ -0,0 +1,76 @@ +# Watchdog + +The Pode Watchdog feature allows you to monitor and manage processes or scripts running within your Pode server. It provides the ability to track the status of a process, log important events, and interact with the process, including via REST API endpoints. + +## Features +- **Process Monitoring**: Continuously track the status, uptime, and performance of processes running under the Pode Watchdog. +- **File Monitoring with Automatic Restart**: Automatically restart a monitored process when changes are detected in files it depends on, such as configuration or critical files. +- **Process Control**: Control the monitored processes through REST API commands such as restart, stop, or reset. +- **Logging**: Watchdog supports logging of important events and errors, which can be useful for auditing and debugging. +- **Automatic Restarts**: If a monitored process crashes unexpectedly, Pode Watchdog will automatically restart it to ensure it remains active. + +### How It Works +Pode Watchdog monitors processes or files as configured in your Pode server. Once a process is being monitored, you can interact with it using commands or REST API endpoints. Watchdog continuously tracks the process and ensures it remains active by automatically restarting it when necessary—especially when critical files change. + +### Typical Use Cases +1. **Process Monitoring**: Monitor long-running scripts or background services and ensure they are continuously running. +2. **File Monitoring with Automatic Restart**: Watch for changes in key files, such as configuration files, and restart the process automatically when changes are detected. +3. **Automatic Restarts**: Ensure critical processes automatically restart if they crash or when monitored files are modified. +4. **Remote Control**: Use API endpoints to control processes remotely—start, stop, reset, or restart them. + +--- + +### Enabling Pode Watchdog +To begin using the Pode Watchdog feature, specify the process or file you wish to monitor. Here’s an example to monitor a script file and automatically restart the process when the file changes: + +```powershell +Enable-PodeWatchdog -FilePath './scripts/myProcess.ps1' -FileMonitoring -FileExclude '*.log' -Name 'myProcessWatchdog' +``` + +- `-FilePath`: Specifies the path to the script or process you want to monitor. +- `-FileMonitoring`: Enables file monitoring to track changes to the specified file. +- `-FileExclude`: Excludes certain files (e.g., `.log` files) from triggering a restart. +- `-Name`: Assigns a unique identifier for this Watchdog instance. + +### **Monitoring** +You can monitor process metrics, such as status, uptime, or other performance data, using the `Get-PodeWatchdogProcessMetric` cmdlet. + +### **Controlling the Watchdog** +Pode Watchdog provides full control over monitored processes using the `Set-PodeWatchdogProcessState` cmdlet, allowing you to restart, stop, start, or reset the process. + +--- + +### **Example Usage with RESTful Integration** + +Here’s an example of how to set up a Pode server with Watchdog and expose REST API routes to monitor and control the process: + +```powershell +Start-PodeServer { + # Define an HTTP endpoint + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Http + + # Path to the monitored script + $filePath = "./scripts/myProcess.ps1" + + # Set up Watchdog logging + New-PodeLoggingMethod -File -Name 'watchdog' -MaxDays 4 | Enable-PodeErrorLogging + + # Enable Watchdog monitoring for the process + Enable-PodeWatchdog -FilePath $filePath -FileMonitoring -FileExclude '*.log' -Name 'myProcessWatchdog' + + # Route to check process status + Add-PodeRoute -Method Get -Path '/monitor/status' -ScriptBlock { + Write-PodeJsonResponse -Value (Get-PodeWatchdogProcessMetric -Name 'myProcessWatchdog' -Type Status) + } + + # Route to restart the process + Add-PodeRoute -Method Post -Path '/cmd/restart' -ScriptBlock { + Write-PodeJsonResponse -Value @{success = (Set-PodeWatchdogProcessState -Name 'myProcessWatchdog' -State Restart)} + } +} +``` + +In this example: +- Pode Watchdog is configured to monitor a script (`myProcess.ps1`). +- The Pode server exposes REST API routes to check the process status (`/monitor/status`) and restart the process (`/cmd/restart`). + diff --git a/examples/Logging.ps1 b/examples/Logging.ps1 index 2625991bf..b0e34a0b0 100644 --- a/examples/Logging.ps1 +++ b/examples/Logging.ps1 @@ -82,4 +82,4 @@ Start-PodeServer { Set-PodeResponseAttachment -Path 'Anger.jpg' } -} +} \ No newline at end of file diff --git a/examples/Watchdog/Watchdog-MultipleInstances.ps1 b/examples/Watchdog/Watchdog-MultipleInstances.ps1 new file mode 100644 index 000000000..16d529cc6 --- /dev/null +++ b/examples/Watchdog/Watchdog-MultipleInstances.ps1 @@ -0,0 +1,51 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with multiple Pode Watchdog instances for process monitoring. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8082 and configures two Pode Watchdog instances to monitor the same script. + It configures logging for the Watchdog service and monitors the provided script file, excluding `.log` files for both instances. + The script dynamically loads the Pode module and enables multiple instances of the Pode Watchdog service for monitoring. + +.EXAMPLE + To run the sample: ./Watchdog-MultipleInstances.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Waatchdog/Watchdog-MultipleInstances.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine paths for the Pode module + $watchdogPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path + $podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $watchdogPath) + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer { + # Define a simple HTTP endpoint on localhost:8082 + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Http + + # Path to the monitored script + $filePath = "$($watchdogPath)/monitored.ps1" + + # Set up logging for the Watchdog service with a 4-day retention period + New-PodeLoggingMethod -File -Name 'watchdog' -MaxDays 4 | Enable-PodeErrorLogging + + # Enable the first Pode Watchdog instance to monitor the script file, excluding .log files + Enable-PodeWatchdog -FilePath $filePath -FileMonitoring -Parameters @{Port = 8080 } -FileExclude '*.log' -Name 'watch01' + + # Enable the second Pode Watchdog instance to monitor the script file, excluding .log files + Enable-PodeWatchdog -FilePath $filePath -FileMonitoring -Parameters @{Port = 8081 } -FileExclude '*.log' -Name 'watch02' +} diff --git a/examples/Watchdog/Watchdog-OpenApiIntegration.ps1 b/examples/Watchdog/Watchdog-OpenApiIntegration.ps1 new file mode 100644 index 000000000..5929ca5ec --- /dev/null +++ b/examples/Watchdog/Watchdog-OpenApiIntegration.ps1 @@ -0,0 +1,150 @@ +<# +.SYNOPSIS + A Pode server setup with OpenAPI integration that monitors a script using the Pode Watchdog service. + +.DESCRIPTION + This script initializes a Pode server with OpenAPI documentation and multiple routes to monitor the status, listeners, requests, + and signals of a monitored process via the Pode Watchdog service. + It also provides commands to control the state of the monitored process, such as restart, stop, reset, and halt. + The script dynamically loads the Pode module and configures OpenAPI viewers and an editor for documentation. + This sample demonstrates the integration of Pode Watchdog with OpenAPI routes for monitoring and controlling processes. + +.EXAMPLE + Run the script to start a Pode server on localhost at port 8082 with OpenAPI documentation: + + ./Watchdog-OpenApiIntegration.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Watchdog/Watchdog-OpenApiIntegration.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine paths for the Pode module + $watchdogPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path + $podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $watchdogPath) + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer -Threads 2 { + # Define a simple HTTP endpoint on localhost:8082 + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Http + + # Enable OpenAPI with OpenAPI version 3.0.3 and disable minimal definitions + Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions + Add-PodeOAInfo -Title 'Pode Watchdog sample' -Version 1.0.0 + + # Enable various OpenAPI viewers for documentation + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' + Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' + Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' + Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' + Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' + + # Enable OpenAPI editor and bookmarks for easier documentation navigation + Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' + Enable-PodeOAViewer -Bookmarks -Path '/docs' + + # Path to the monitored script + $filePath = "$($watchdogPath)/monitored.ps1" + + # Set up logging for the Watchdog service + New-PodeLoggingMethod -File -Name 'watchdog' -MaxDays 4 | Enable-PodeErrorLogging + + # Enable the Pode Watchdog to monitor the script file, excluding .log files + Enable-PodeWatchdog -FilePath $filePath -Parameters @{Port = 8081 } -FileMonitoring -FileExclude '*.log' -Name 'watch01' -RestartServiceAfter 10 -MaxNumberOfRestarts 2 -ResetFailCountAfter 3 + + # Define OpenAPI schemas for request, listener, signal, and status metrics + $WatchdogSchemaPrefix = 'Watchdog' + Add-PodeWatchdogOASchema -WatchdogSchemaPrefix $WatchdogSchemaPrefix + + # REST API to retrieve the list of listeners + Add-PodeRoute -PassThru -Method Get -Path '/monitor/listeners' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value (Get-PodeWatchdogProcessMetric -Name 'watch01' -type Listeners) + } | Set-PodeOARouteInfo -Summary 'Retrieves a list of active listeners for the monitored Pode server' -Tags 'Monitor' -OperationId 'getListeners' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = "$($WatchdogSchemaPrefix)Listeners" } + + # REST API to retrieve the request count + Add-PodeRoute -PassThru -Method Get -Path '/monitor/requests' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value (Get-PodeWatchdogProcessMetric -Name 'watch01' -type Requests) + } | Set-PodeOARouteInfo -Summary 'Retrieves the total number of requests handled by the monitored Pode server' -Tags 'Monitor' -OperationId 'getRequests' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = "$($WatchdogSchemaPrefix)Requests" } + + # REST API to retrieve the process status + Add-PodeRoute -PassThru -Method Get -Path '/monitor/status' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value (Get-PodeWatchdogProcessMetric -Name 'watch01' -type Status) + } | Set-PodeOARouteInfo -Summary 'Retrieves the current status and uptime of the monitored Pode server' -Tags 'Monitor' -OperationId 'getStatus' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = "$($WatchdogSchemaPrefix)Status" } + + # REST API to retrieve signal metrics + Add-PodeRoute -PassThru -Method Get -Path '/monitor/signals' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value (Get-PodeWatchdogProcessMetric -Name 'watch01' -type Signals) + } | Set-PodeOARouteInfo -Summary 'Retrieves signal metrics for the monitored Pode server' -Tags 'Monitor' -OperationId 'getSignals' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = "$($WatchdogSchemaPrefix)Signals" } + + # REST API to retrieve all metrics of the monitored process + Add-PodeRoute -PassThru -Method Get -Path '/monitor' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value (Get-PodeWatchdogProcessMetric -Name 'watch01') + } | Set-PodeOARouteInfo -Summary 'Retrieves all monitoring stats for the monitored Pode server' -Tags 'Monitor' -OperationId 'getMonitor' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{'application/json' = "$($WatchdogSchemaPrefix)Monitor" } + + # REST API to restart the monitored process + Add-PodeRoute -PassThru -Method Post -Path '/cmd/restart' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value @{ success = (Set-PodeWatchdogProcessState -Name 'watch01' -State Restart) } + } | Set-PodeOARouteInfo -Summary 'Restarts the monitored Pode server' -Tags 'Command' -OperationId 'restart' + + # REST API to reset the monitored process + Add-PodeRoute -PassThru -Method Post -Path '/cmd/reset' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value @{ success = (Set-PodeWatchdogProcessState -Name 'watch01' -State Reset) } + } | Set-PodeOARouteInfo -Summary 'Stops and restarts the monitored Pode server process' -Tags 'Command' -OperationId 'reset' + + # REST API to stop the monitored process + Add-PodeRoute -PassThru -Method Post -Path '/cmd/stop' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value @{ success = (Set-PodeWatchdogProcessState -Name 'watch01' -State Stop) } + } | Set-PodeOARouteInfo -Summary 'Stops the monitored Pode server process' -Tags 'Command' -OperationId 'stop' + + # REST API to start the monitored process + Add-PodeRoute -PassThru -Method Post -Path '/cmd/start' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value @{ success = (Set-PodeWatchdogProcessState -Name 'watch01' -State Start) } + } | Set-PodeOARouteInfo -Summary 'Starts the monitored Pode server process' -Tags 'Command' -OperationId 'start' + + # REST API to terminate (force stop) the monitored process + Add-PodeRoute -PassThru -Method Post -Path '/cmd/terminate' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value @{ success = (Set-PodeWatchdogProcessState -Name 'watch01' -State Terminate) } + } | Set-PodeOARouteInfo -Summary 'Terminates (force stops) the monitored Pode server process' -Tags 'Command' -OperationId 'terminate' + + # REST API to disable the monitored process + Add-PodeRoute -PassThru -Method Post -Path '/cmd/disable' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value @{ success = (Set-PodeWatchdogProcessState -Name 'watch01' -State Disable) } + } | Set-PodeOARouteInfo -Summary 'Disables the monitored Pode server process' -Tags 'Command' -OperationId 'disable' + + # REST API to enable the monitored process + Add-PodeRoute -PassThru -Method Post -Path '/cmd/enable' -ScriptBlock { + Write-PodeJsonResponse -StatusCode 200 -Value @{ success = (Set-PodeWatchdogProcessState -Name 'watch01' -State Enable) } + } | Set-PodeOARouteInfo -Summary 'Enables the monitored Pode server process' -Tags 'Command' -OperationId 'enable' + + # REST API to disable Auto Restart + Add-PodeRoute -PassThru -Method Post -Path '/settings/noAutoRestart' -ScriptBlock { + Disable-PodeWatchdogAutoRestart -Name 'watch01' + Set-PodeResponseStatus -Code 200 + } | Set-PodeOARouteInfo -Summary 'Disables the auto-restart feature for the monitored Pode server process' -Tags 'Settings' -OperationId 'noAutoRestart' + + # REST API to enable Auto Restart + Add-PodeRoute -PassThru -Method Post -Path '/settings/autoRestart' -ScriptBlock { + Enable-PodeWatchdogAutoRestart -Name 'watch01' + Set-PodeResponseStatus -Code 200 + } | Set-PodeOARouteInfo -Summary 'Enables the auto-restart feature for the monitored Pode server process' -Tags 'Settings' -OperationId 'autoRestart' + +} diff --git a/examples/Watchdog/Watchdog-SingleInstance.ps1 b/examples/Watchdog/Watchdog-SingleInstance.ps1 new file mode 100644 index 000000000..9b3ee393d --- /dev/null +++ b/examples/Watchdog/Watchdog-SingleInstance.ps1 @@ -0,0 +1,47 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with a Pode Watchdog for process monitoring. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8082 and monitors a script using the Pode Watchdog service. + It configures logging for the Watchdog service and monitors the provided script file, excluding `.log` files. + The script dynamically loads the Pode module and sets up basic process monitoring using the Pode Watchdog. + +.EXAMPLE + To run the sample: ./Watchdog-Sample.ps1 + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Waatchdog/Watchdog-SingleInstance.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + + +try { + # Determine paths for the Pode module + $watchdogPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path + $podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $watchdogPath) + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +Start-PodeServer { + # Define a simple HTTP endpoint on localhost:8082 + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Http + + # Path to the monitored script + $filePath = "$($watchdogPath)/monitored.ps1" + + # Enable the Pode Watchdog to monitor the script file, excluding .log files + Enable-PodeWatchdog -FilePath $filePath -Parameters @{Port = 8081 } -FileMonitoring -FileExclude '*.log' -Name 'watch01' + +} diff --git a/examples/Watchdog/monitored.ps1 b/examples/Watchdog/monitored.ps1 new file mode 100644 index 000000000..cd994b4a0 --- /dev/null +++ b/examples/Watchdog/monitored.ps1 @@ -0,0 +1,77 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with a simple GET endpoint. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8080. It includes a single route for GET requests + to the root path ('/') that returns a simple text response. + +.PARAMETER Port + The port to listen on for requests. Defaults to 8080. + +.EXAMPLE + To run the sample: ./HelloWorld/HelloWorld.ps1 + + # HTML responses 'Hello, world! + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/HelloWorld/HelloWorld.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +param( + [int] + $Port = 8080 +) +try { + # Get the path of the script being executed + $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)) + # Get the parent directory of the script's path + $podePath = Split-Path -Parent -Path $ScriptPath + + # Check if the Pode module file exists in the specified path + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + # If the Pode module file exists, import it + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + # If the Pode module file does not exist, import the Pode module from the system + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { + # If there is any error during the module import, throw the error + throw +} + +# Alternatively, you can directly import the Pode module from the system +# Import-Module Pode + +# Start the Pode server +Start-PodeServer -Threads 10 { + # Add an HTTP endpoint listening on localhost at port 8080 + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http + + # Add a route for GET requests to the root path '/' + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # Send a text response with 'Hello, world!' + Write-PodeTextResponse -Value 'Hello, world!' + } + + Add-PodeRoute -Method Get -Path '/json' -ScriptBlock { + # Send a json response with 'Hello, world! + Write-PodeJsonResponse -Value @{response = 'Hello, world! in json' } + } + + Add-PodeRoute -Method Get -Path '/delayed' -ScriptBlock { + # Send a json response with 'Hello, world!' + $sleep = (Get-Random -Minimum 10 -Maximum 30) + start-sleep $sleep + Write-PodeJsonResponse -Value @{response = "Hello, world! ($sleep seconds delay)" } + } + +} diff --git a/src/Pode.psd1 b/src/Pode.psd1 index aff2bfcb7..009afe004 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -534,6 +534,17 @@ 'New-PodeLimitEndpointComponent', 'New-PodeLimitMethodComponent', 'New-PodeLimitHeaderComponent' + 'Use-PodeScopedVariables', + + # watchdog + 'Enable-PodeWatchdog', + 'Test-PodeWatchdog', + 'Get-PodeWatchdogProcessMetric', + 'Set-PodeWatchdogProcessState', + 'Enable-PodeWatchdogAutoRestart', + 'Disable-PodeWatchdogAutoRestart', + 'Add-PodeWatchdogOASchema' + ) # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 19d4c1004..9bdd34512 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -55,6 +55,9 @@ function New-PodeContext { [string] $ConfigFile, + [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]] + $Watchdog, + [switch] $Daemon ) @@ -101,6 +104,13 @@ function New-PodeContext { $ctx.Server.Console = $Console $ctx.Server.ComputerName = [System.Net.DNS]::GetHostName() + + if ($Watchdog) { + $ctx.Server.Watchdog = @{ + Client = $Watchdog + } + } + # list of created listeners/receivers $ctx.Listeners = @() $ctx.Receivers = @() @@ -149,6 +159,7 @@ function New-PodeContext { Tasks = 2 WebSockets = 2 Timers = 1 + Watchers = 0 } # set socket details for pode server @@ -501,6 +512,7 @@ function New-PodeContext { Tasks = $null Files = $null Timers = $null + Watchdog = $null } # threading locks, etc. @@ -707,6 +719,14 @@ function New-PodeRunspacePool { $PodeContext.RunspacePools.Gui.Pool.ApartmentState = 'STA' } + + if (Test-PodeWatchdogEnabled ) { + $PodeContext.Threads['Watchdog'] = Get-PodeWatchdogRunspaceCount + $PodeContext.RunspacePools.Watchdog = @{ + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads['Watchdog'], $PodeContext.RunspaceState, $Host) + State = 'Waiting' + } + } } <# diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index beefebb89..813a38b2a 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3736,7 +3736,7 @@ function Resolve-PodeObjectArray { # Check if the property is a hashtable if ($Property -is [hashtable]) { # If the hashtable has only one item, convert it to a PowerShell object - if ($Property.Count -eq 1) { + if (($Property.GetEnumerator() | Measure-Object).Count -eq 1) { return [pscustomobject]$Property } else { @@ -4009,6 +4009,77 @@ function Test-PodeIsISEHost { return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name)) } + +<# +.SYNOPSIS + Converts a hashtable to a ConcurrentDictionary. + +.DESCRIPTION + The `ConvertTo-PodeConcurrentStructure` function takes a hashtable and converts it into a + ConcurrentDictionary, which provides thread-safe operations for adding and retrieving items. + This function supports the recursive conversion of nested hashtables, ordered dictionaries, and arrays. + +.PARAMETER InputObject + The hashtable to be converted into a ConcurrentDictionary. + +.OUTPUTS + [System.Collections.Concurrent.ConcurrentDictionary[string, object]] + Returns a ConcurrentDictionary with the same keys and values as the input hashtable. + +.EXAMPLE + $hashTable = @{ + Key1 = 'Value1' + Key2 = @{ + SubKey1 = 'SubValue1' + } + } + $concurrentDictionary = ConvertTo-PodeConcurrentStructure -InputObject $hashTable + # The variable $concurrentDictionary now contains a ConcurrentDictionary with the same structure as $hashTable + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertTo-PodeConcurrentStructure { + param ( + [object] + $InputObject + ) + + if ($InputObject -is [hashtable]) { + $concurrentDictionary = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new([StringComparer]::OrdinalIgnoreCase) + } + elseif ($InputObject -is [System.Collections.Specialized.OrderedDictionary]) { + $concurrentDictionary = [Pode.PodeOrderedConcurrentDictionary[string, object]]::new([StringComparer]::OrdinalIgnoreCase) + } + elseif ($InputObject -is [System.Object[]]) { + $array = [System.Collections.ArrayList]::Synchronized([System.Collections.ArrayList]::new()) + foreach ($item in $InputObject) { + # Convert each item and add to the synchronized array + $convertedItem = ConvertTo-PodeConcurrentStructure -InputObject $item + [void]$array.Add($convertedItem) + } + + # Return synchronized ArrayList without unrolling + return , $array + } + else { + # If the object is neither a hashtable, ordered dictionary, nor array, return it as-is + return $InputObject + } + + foreach ($key in $InputObject.Keys) { + $value = $InputObject[$key] + + # Recursively convert nested hashtables, ordered dictionaries, or arrays + if ($value -is [hashtable] -or $value -is [System.Collections.Specialized.OrderedDictionary] -or $value -is [System.Object[]]) { + $value = ConvertTo-PodeConcurrentStructure -InputObject $value + } + + $concurrentDictionary[$key] = $value + } + + return $concurrentDictionary +} <# .SYNOPSIS Determines the MIME type of an image from its binary header. diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index da4bd0b46..b7845273a 100644 --- a/src/Private/Runspaces.ps1 +++ b/src/Private/Runspaces.ps1 @@ -42,7 +42,6 @@ function Add-PodeRunspace { param( [Parameter(Mandatory = $true)] - [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files', 'Timers')] [string] $Type, diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 0725b2e67..aa71ca25b 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -46,6 +46,9 @@ function Start-PodeInternalServer { # run start event hooks Invoke-PodeEvent -Type Starting + # Indicating that the Watchdog client is starting + Set-PodeWatchdogHearthbeatStatus -Status 'Starting' + # setup temp drives for internal dirs Add-PodePSInbuiltDrive @@ -68,6 +71,7 @@ function Start-PodeInternalServer { } $_script = Convert-PodeScopedVariables -ScriptBlock $_script -Exclude Session, Using + $null = Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure -Splat #Validate OpenAPI definitions @@ -195,6 +199,12 @@ function Start-PodeInternalServer { # run running event hooks Invoke-PodeEvent -Type Running + # Displays startup information for the Pode Watchdog service. + Write-PodeWatchdogStartupMessage + + # Marking the Watchdog client as 'Running' now that the process is stable + Set-PodeWatchdogHearthbeatStatus -Status 'Running' + } catch { @@ -229,13 +239,21 @@ function Restart-PodeInternalServer { # Restarting server... Show-PodeConsoleInfo + # Setting the Watchdog status to 'Restarting' as part of the process recovery + Set-PodeWatchdogHearthbeatStatus -Status 'Restarting' + # run restarting event hooks Invoke-PodeEvent -Type Restarting # cancel the session token Close-PodeCancellationTokenRequest -Type Cancellation, Terminate + # stop the watchdog if it's running + Write-Verbose 'Stopping watchdog' + Stop-PodeWatchdog + # close all current runspaces + Write-Verbose 'Closing runspaces' Close-PodeRunspace -ClosePool # remove all of the pode temp drives @@ -643,4 +661,53 @@ function Disable-PodeServerInternal { function Test-PodeServerIsEnabled { return !(Test-PodeLimitRateRule -Name $PodeContext.Server.AllowedActions.DisableSettings.LimitRuleName) +} + + +function Close-PodeServerInternal { + param( + [switch] + $ShowDoneMessage + ) + + # ensure the token is cancelled + if ($null -ne $PodeContext.Tokens.Cancellation) { + Write-Verbose 'Cancelling main cancellation token' + $PodeContext.Tokens.Cancellation.Cancel() + } + + # stop the watchdog if it's running + Write-Verbose 'Stopping watchdog' + Stop-PodeWatchdog + + # stop all current runspaces + Write-Verbose 'Closing runspaces' + Close-PodeRunspace -ClosePool + + # stop the file monitor if it's running + Write-Verbose 'Stopping file monitor' + Stop-PodeFileMonitor + + try { + # remove all the cancellation tokens + Write-Verbose 'Disposing cancellation tokens' + Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation + Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart + + # dispose mutex/semaphores + Write-Verbose 'Diposing mutex and semaphores' + Clear-PodeMutexes + Clear-PodeSemaphores + } + catch { + $_ | Out-Default + } + + # remove all of the pode temp drives + Write-Verbose 'Removing internal PSDrives' + Remove-PodePSDrive + + if ($ShowDoneMessage -and ($PodeContext.Server.Types.Length -gt 0) -and !$PodeContext.Server.IsServerless) { + Write-PodeHost $PodeLocale.doneMessage -ForegroundColor Green + } } \ No newline at end of file diff --git a/src/Private/Watchdog.ps1 b/src/Private/Watchdog.ps1 new file mode 100644 index 000000000..29c4f5d88 --- /dev/null +++ b/src/Private/Watchdog.ps1 @@ -0,0 +1,894 @@ +<# +.SYNOPSIS + Checks if the Pode Watchdog service is enabled for either the client or server. + +.DESCRIPTION + This internal function checks whether the Pode Watchdog service is enabled by verifying if the 'Watchdog' key exists in the `PodeContext.Server`. + It can check specifically for the client or server component, or both, based on the provided parameter set. + +.PARAMETER Client + Checks if the Pode Watchdog client component is enabled. + +.PARAMETER Server + Checks if the Pode Watchdog server component is enabled. + +.OUTPUTS + [boolean] + Returns $true if the Watchdog service is enabled for the requested component (client or server), otherwise returns $false. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeWatchDogEnabled { + [CmdletBinding(DefaultParameterSetName = 'Builtin')] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Client')] + [switch] + $Client, + + [Parameter(Mandatory = $true, ParameterSetName = 'Server')] + [switch] + $Server + ) + + # Check if the Watchdog Client is enabled + if ($Client.IsPresent) { + return $PodeContext.Server.containsKey('Watchdog') -and $PodeContext.Server.Watchdog.containsKey('Client') + } + + # Check if the Watchdog Server is enabled + if ($Server.IsPresent) { + return $PodeContext.Server.containsKey('Watchdog') -and $PodeContext.Server.Watchdog.containsKey('Server') + } + + # Check if any Watchdog component is enabled + return $PodeContext.Server.containsKey('Watchdog') +} + +<# +.SYNOPSIS + Stops a monitored process managed by a Pode Watchdog service. + +.DESCRIPTION + This internal function attempts to stop a monitored process managed by a Pode Watchdog. + It supports both graceful shutdowns through inter-process communication (IPC) via pipes and forced termination. + +.PARAMETER Name + The name of the Watchdog service managing the monitored process. + +.PARAMETER Timeout + The timeout period (in seconds) to wait for the process to shut down gracefully before returning a failure. + Default is 30 seconds. + +.PARAMETER Force + If specified, the process will be forcibly terminated without a graceful shutdown. + +.OUTPUTS + [boolean] + Returns $true if the process was stopped successfully, otherwise $false. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Stop-PodeWatchdogMonitoredProcess { + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [int] + $Timeout = 30, + + [switch] + $Force + ) + + # If the Watchdog with the specified name is not found, exit the function + if (!(Test-PodeWatchdog -Name $Name)) { + return + } + + # Retrieve the Watchdog instance from the Pode context + $watchdog = $PodeContext.Server.Watchdog.Server[$Name] + + # If the Force switch is specified, forcibly terminate the process + if ($Force.IsPresent) { + if (@('Stopping', 'Stopped') -icontains $watchdog.ProcessInfo.Status ) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Cannot Terminate a process in $($watchdog.ProcessInfo.Status) state." + return $false + } + if ($null -ne $watchdog.Process) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Try to stop the process forcefully using the process ID($($watchdog.Process.Id))" + # Try to stop the process forcefully using its ID + $stoppedProcess = Get-Process -Id $watchdog.Process.Id -ErrorAction SilentlyContinue + if ($null -ne $stoppedProcess) { + $watchdog.ProcessInfo.Status = 'Stopping' + $stoppedProcess = Stop-Process -Id $watchdog.Process.Id -PassThru -ErrorAction SilentlyContinue + if ($null -eq $stoppedProcess) { + return $false # Return false if the process could not be stopped + } + } + # Clear the process information and update the status to 'Stopped' + $watchdog.Process = $null + $watchdog.ProcessInfo.Status = 'Stopped' + return $true + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'No watchdog process found' # Log if no process is found for the Watchdog + } + + return $false + } + + try { + if ( $watchdog.ProcessInfo.Status -ne 'Running' ) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Cannot stop a process that is not in running state.' + return $false + } + # Attempt graceful shutdown via pipe communication + if (! (Send-PodeWatchdogMessage -Name $Name -Command 'shutdown')) { + return $false + } + # Wait for the process to exit within the specified timeout + $counter = 0 + $process = Get-Process -Id $watchdog.Process.Id -ErrorAction SilentlyContinue + while ($null -ne $process) { + Start-Sleep -Seconds 1 + $process = Get-Process -Id $watchdog.Process.Id -ErrorAction SilentlyContinue + $counter++ + if ($counter -ge $Timeout) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Stop-PodeWatchdogMonitoredProcess timeout reached' # Log timeout + return $false + } + } + + # Clear process information and update status upon successful shutdown + $watchdog.Process = $null + return $true + } + catch { + # Log any errors that occur during the shutdown process + $_ | Write-PodeWatchdogLog -Watchdog $watchdog + return $false + } +} + + +<# +.SYNOPSIS + Restarts a monitored process managed by a Pode Watchdog service. + +.DESCRIPTION + This internal function sends a restart command to a monitored process managed by a Pode Watchdog service via inter-process communication (IPC) using pipes. + It waits for the process to restart and verifies that the restart was successful by checking the restart count. + +.PARAMETER Name + The name of the Watchdog service managing the monitored process. + +.PARAMETER Timeout + The timeout period (in seconds) to wait for the process to restart gracefully before returning a failure. + Default is 30 seconds. + +.OUTPUTS + [bool] + Returns $true if the process was restarted successfully, otherwise $false. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Restart-PodeWatchdogMonitoredProcess { + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [int] + $Timeout = 30 + ) + + # If the Watchdog with the specified name is not found, exit the function + if (!(Test-PodeWatchdog -Name $Name)) { + return + } + + # Retrieve the Watchdog instance from the Pode context + $watchdog = $PodeContext.Server.Watchdog.Server[$Name] + + try { + if ( $watchdog.ProcessInfo.Status -ne 'Running' ) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Cannot restart a process that is not in running state.' + return $false + } + + $restartCount = $watchdog.ProcessInfo.RestartCount + # Attempt to restart the monitored process via pipe communication + if (! (Send-PodeWatchdogMessage -Name $Name -Command 'Restart')) { + return $false + } + + # Initialize counter for the first check (Running) + $counter = 0 + # Wait for the monitored process to update its process info after restarting + while ($watchdog.ProcessInfo.Status -eq 'Running' -and $counter -lt $Timeout) { + Start-Sleep 1 + $counter++ + + # Exit the loop if timeout is reached + if ($counter -ge $Timeout) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Timeout ($Timeout secs) reached while waiting for the process to stop running." + return $false + } + } + + # Wait for the process to stop restarting + while ($watchdog.ProcessInfo.Status -eq 'Restarting' -and $counter -lt $Timeout) { + Start-Sleep 1 + $counter++ + + # Exit the loop if timeout is reached + if ($counter -ge $Timeout) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Timeout ($Timeout secs) reached while waiting for the process to stop running." + return $false + } + } + + # Verify that the restart count has incremented, indicating a successful restart + return ($watchdog.ProcessInfo.RestartCount -eq ($restartCount + 1)) + } + catch { + # Log any errors that occur during the restart process + $_ | Write-PodeWatchdogLog -Watchdog $watchdog + return $false + } +} + +<# +.SYNOPSIS + Starts a monitored process managed by a Pode Watchdog service. + +.DESCRIPTION + This internal function starts the process that is monitored by the specified Pode Watchdog service. + It uses the configured shell and arguments stored in the Watchdog context to start the process and updates the Watchdog's status accordingly. + +.PARAMETER Name + The name of the Watchdog service managing the monitored process. + +.OUTPUTS + [bool] + Returns $true if the process was started successfully, otherwise $false. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Start-PodeWatchdogMonitoredProcess { + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # If the Watchdog with the specified name is not found, exit the function + if (!(Test-PodeWatchdog -Name $Name)) { + return + } + + # Retrieve the Watchdog instance from the Pode context + $watchdog = $PodeContext.Server.Watchdog.Server[$Name] + + if ( $watchdog.ProcessInfo.Status -ne 'Stopped') { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Cannot start a process in $($watchdog.ProcessInfo.Status) state." + return $false + } + + # Check if the monitored process is not already running + if ($null -eq $watchdog.Process) { + $watchdog.ProcessInfo.Status = 'Starting' + + # Start the monitored process using the shell and arguments from the Watchdog context + $watchdog.Process = Start-Process -FilePath $watchdog.Shell -ArgumentList $watchdog.Arguments -NoNewWindow -PassThru + + $watchdog.ProcessInfo.Pid = $watchdog.Process.Id + $watchdog.ProcessInfo.Accessible = $false + + # Increment the restart count + $watchdog.RestartCount = $watchdog.RestartCount + 1 + + + # Output the process information for debugging purposes + Write-PodeWatchdogLog -Watchdog $watchdog -Message (( + $watchdog.Process | Select-Object @{Name = 'NPM(K)'; Expression = { $_.NPM } }, + @{Name = 'PM(M)'; Expression = { [math]::round($_.PM / 1MB, 2) } }, + @{Name = 'WS(M)'; Expression = { [math]::round($_.WS / 1MB, 2) } }, + @{Name = 'CPU(s)'; Expression = { [math]::round($_.CPU, 2) } }, + Id, SI, ProcessName + ) -join ',') + + # Check if the process was successfully started and is running + if (!$watchdog.Process.HasExited) { + return $true + } + else { + # If the process has already exited, mark the status as 'Stopped' + $watchdog.ProcessInfo.Status = 'Stopped' + } + } + + # Return false if the process could not be started or was already running + return $false +} + +<# +.SYNOPSIS + Starts the runspaces for all active Pode Watchdog services. + +.DESCRIPTION + This internal function iterates through all active Watchdog services in the Pode context, starts their monitored processes, and initializes a runspace for each Watchdog using the provided script block. + +.OUTPUTS + None + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Start-PodeWatchdogRunspace { + # Iterate through each Watchdog service in the Pode context + foreach ($name in $PodeContext.Server.Watchdog.Server.Keys) { + $watchdog = $PodeContext.Server.Watchdog.Server[$name] + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Starting Watchdog $name" + # Start the monitored process for the Watchdog + $null = Start-PodeWatchdogMonitoredProcess -Name $name + + # Initialize a runspace for the Watchdog using the provided ScriptBlock + $watchdog.Runspace = Add-PodeRunspace -Type 'Watchdog' ` + -ScriptBlock ($watchdog.ScriptBlock) ` + -Parameters @{'WatchdogName' = $watchdog.Name } ` + -PassThru + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Watchdog $name started" + } + +} + + +<# +.SYNOPSIS + Stops the runspaces and monitored processes for all active Pode Watchdog services. + +.DESCRIPTION + This internal function iterates through all active Watchdog services in the Pode context, stops their monitored processes, disables their runspaces, and cleans up any resources such as pipe servers and writers. + +.OUTPUTS + None + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Stop-PodeWatchdogRunspace { + + # Iterate through each Watchdog service in the Pode context + foreach ($name in $PodeContext.Server.Watchdog.Server.Keys) { + $watchdog = $PodeContext.Server.Watchdog.Server[$name] + # Disable the Watchdog service + $watchdog.Enabled = $false + # Disable autorestart + $watchdog.AutoRestart.Enabled = $false + + + # Attempt to stop the monitored process and update the status accordingly + $null = Stop-PodeWatchdogMonitoredProcess -Name $name -Timeout 60 + + # Clean up the PipeWriter if it exists + if ($null -ne $watchdog.PipeWriter) { + try { + $watchdog.PipeWriter.Dispose() + } + catch { + $_ | Write-PodeWatchdogLog -Watchdog $watchdog -Level Verbose # Log errors during disposal of PipeWriter + } + } + + # Clean up the PipeServer if it exists + if ($null -ne $watchdog.PipeServer) { + try { + # Disconnect the PipeServer if it is still connected + if ($watchdog.PipeServer.IsConnected()) { + $watchdog.PipeServer.Disconnect() + } + } + catch { + $_ | Write-PodeWatchdogLog -Watchdog $watchdog -Level Verbose # Log errors during disconnection of PipeServer + } + + # Dispose of the PipeServer + try { + $watchdog.PipeServer.Dispose() + } + catch { + $_ | Write-PodeWatchdogLog -Watchdog $watchdog -Level Verbose # Log errors during disposal of PipeServer + } + } + } +} + +<# +.SYNOPSIS + Stops the Pode Watchdog service, including both client and server components. + +.DESCRIPTION + This internal function checks if the Pode Watchdog service is running and stops both the client (heartbeat) and server (runspace) components if they exist. + +.OUTPUTS + None + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Stop-PodeWatchdog { + + # Check if the Watchdog service exists in the Pode context + if ($PodeContext.Server.containsKey('Watchdog')) { + # Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Stopping watchdog' + + # Stop the Watchdog client (heartbeat) if it exists + if ($PodeContext.Server.Watchdog.containsKey('Client')) { + Stop-PodeWatchdogHearthbeat + } + + # Stop the Watchdog server (runspace) if it exists + if ($PodeContext.Server.Watchdog.containsKey('Server')) { + Stop-PodeWatchdogRunspace + } + } +} + +<# +.SYNOPSIS + Starts the Pode Watchdog service, including both client and server components. + +.DESCRIPTION + This internal function checks if the Pode Watchdog service is running and starts both the client (heartbeat) and server (runspace) components if they exist. + +.OUTPUTS + None + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Start-PodeWatchdog { + + # Check if the Watchdog service exists in the Pode context + if ($PodeContext.Server.containsKey('Watchdog')) { + + # Start the Watchdog client (heartbeat) if it exists + if ($PodeContext.Server.Watchdog.containsKey('Client')) { + # Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Starting Client watchdog' + Start-PodeWatchdogHearthbeat + } + + # Start the Watchdog server (runspace) if it exists + if ($PodeContext.Server.Watchdog.containsKey('Server')) { + # Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Starting Server watchdog' + Start-PodeWatchdogRunspace + } + } +} + +<# +.SYNOPSIS + Sends a command to the monitored process via the Pode Watchdog pipe. + +.DESCRIPTION + This function sends a specified command to the monitored process through the Watchdog pipe communication. It ensures the command is delivered immediately via the PipeWriter. + If the pipe is disconnected, the function logs the failure and returns $false. + + .PARAMETER Name + The name of the Watchdog service managing the monitored process. + +.PARAMETER Command + The command to be sent to the monitored process. This could be commands like 'restart', 'shutdown', etc. + +.OUTPUTS + [boolean] + Returns $true if the command was successfully sent via the pipe, otherwise returns $false if the connection was lost. + +.EXAMPLE + Send-PodeWatchdogMessage -Name 'Watcher01' -Command 'restart' + + Sends a 'restart' command to the monitored process through the Watchdog pipe. + +.NOTES + This function is used for communication with monitored processes in Pode and may change in future releases of Pode. +#> +function Send-PodeWatchdogMessage { + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $Command + ) + # Retrieve the Watchdog instance from the Pode context + $watchdog = $PodeContext.Server.Watchdog.Server[$Name] + + # Attempt to send the command to the monitored process via pipe communication + if ($watchdog.PipeServer.IsConnected) { + + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Send the command '$Command' to the monitored process via the PipeWriter." + # Send the command to the monitored process via the PipeWriter + $watchdog.PipeWriter.WriteLine($Command) + $watchdog.PipeWriter.Flush() # Ensure the message is sent immediately + return $true + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Pipe connection lost. Command '$Command' cannot be delivered." + return $false + } +} + +<# +.SYNOPSIS + Retrieves the total number of active Pode Watchdog runspaces. + +.DESCRIPTION + This internal function calculates the total number of active Watchdog runspaces by counting both the client and server components of the Pode Watchdog service. + The function is used to update the number of running threads in `$PodeContext.Threads['Watchdog']`. + +.OUTPUTS + [int] + Returns the total count of active Watchdog runspaces. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeWatchdogRunspaceCount { + # Initialize the total runspace count + $totalWatchdogRunspaces = 0 + + # Check if the Watchdog client exists and add 1 to the total count + if ($PodeContext.Server.Watchdog.containsKey('Client')) { + $totalWatchdogRunspaces += 1 + } + + # Check if the Watchdog server exists and add the count of server runspaces + if ($PodeContext.Server.Watchdog.containsKey('Server')) { + $totalWatchdogRunspaces += $PodeContext.Server.Watchdog.Server.Count + } + + # Return the total number of Watchdog runspaces + return $totalWatchdogRunspaces +} + +<# +.SYNOPSIS + Returns the script block for initializing and running the Pode Watchdog PipeServer. + +.DESCRIPTION + This function returns a script block that initializes the Pode Watchdog PipeServer and manages communication between the server and the monitored process. + It handles the creation of the PipeServer, sending and receiving messages through pipes, managing process information, and restarting the monitored process based on the AutoRestart settings. + +.OUTPUTS + [scriptblock] + A script block that initializes and runs the Watchdog PipeServer. + +.NOTES + This function is used internally to manage Watchdog server communication and may change in future releases of Pode. +#> +function Get-PodeWatchdogPipeServerScriptBlock { + # Main script block to initialize and run the Watchdog PipeServer + return [scriptblock] { + param ( + [string] + $WatchdogName + ) + + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Starting PipeServer $WatchdogName..." + $watchdog = $PodeContext.Server.Watchdog.Server[$WatchdogName] + + # Main loop to maintain the server state + while ($watchdog.Enabled) { + try { + # Check if PipeServer is null and create a new instance if needed + if ($null -eq $watchdog['PipeServer']) { + $pipeName = $watchdog.PipeName + $watchdog['PipeServer'] = [System.IO.Pipes.NamedPipeServerStream]::new( + $pipeName, + [System.IO.Pipes.PipeDirection]::InOut, + 2, + [System.IO.Pipes.PipeTransmissionMode]::Byte, + [System.IO.Pipes.PipeOptions]::None + ) + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'New PipeServer instance created and stored in Watchdog context.' + + # Initialize the StreamWriter when PipeServer is set + $watchdog['PipeWriter'] = [System.IO.StreamWriter]::new($watchdog['PipeServer']) + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'New PipeWriter instance created and stored in Watchdog context.' + } + + $pipeServer = $watchdog['PipeServer'] + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'PipeServer created and waiting for connection...' + $pipeServer.WaitForConnection() + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Client connected.' + + # Create a StreamReader to read messages from the client + $reader = [System.IO.StreamReader]::new($pipeServer) + + while ($pipeServer.IsConnected) { + try { + # Read the next message from the client + $receivedData = $reader.ReadLine() + + if ($null -ne $receivedData) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Server Received data: $receivedData" + # Deserialize received JSON string into a hashtable + $watchdog.ProcessInfo = $receivedData | ConvertFrom-Json + + # Handle AutoRestart settings based on uptime and restart count + if ($watchdog.AutoRestart.RestartCount -gt 0 -and $watchdog.ProcessInfo.CurrentUptime -ge $watchdog.AutoRestart.ResetFailCountAfter) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Process uptime exceeds threshold. Resetting fail counter.' + $watchdog.AutoRestart.RestartCount = 0 + } + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'No data received from client. Waiting for more data...' + } + } + catch { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Error reading from client: $_" + $pipeServer.Disconnect() # Disconnect to allow reconnection + Start-Sleep -Seconds 1 + } + } + + # Client disconnected, clean up + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Client disconnected. Waiting for a new connection...' + } + catch { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Error with the pipe server: $_" + } + finally { + if ($watchdog.Enabled) { + # Handle monitored process state reporting and AutoRestart logic + $reportedStatus = $watchdog.ProcessInfo.Status + if ($reportedStatus -eq 'Running') { + if ($null -eq (Get-Process -Id $watchdog.Process.Id -ErrorAction SilentlyContinue)) { + $watchdog.ProcessInfo.Status = 'Stopped' + $watchdog.ProcessInfo.Accessible = $false + $watchdog.ProcessInfo.Pid = '' + $watchdog.Process = $null + } + else { + $processInfo.Status = 'Undefined' + $watchdog.Process = $null + } + } + + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Monitored process was reported to be $reportedStatus." + + if ($watchdog.AutoRestart.Enabled) { + if ($reportedStatus -eq 'Running') { + if ($watchdog.AutoRestart.RestartCount -le $watchdog.AutoRestart.MaxNumberOfRestarts) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Waiting $($watchdog.AutoRestart.RestartServiceAfter) seconds before restarting the monitored process" + Start-Sleep -Seconds $watchdog.AutoRestart.RestartServiceAfter + + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Restarting the monitored process...' + if (Stop-PodeWatchdogMonitoredProcess -Name $watchdog.Name -Force) { + Start-PodeWatchdogMonitoredProcess -Name $watchdog.Name + $watchdog.AutoRestart.RestartCount += 1 + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Monitored process (ID: $($watchdog.Process.Id)) restarted ($($watchdog.AutoRestart.RestartCount) time(s)) successfully." + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Failed to restart the monitored process.' + } + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'The monitored process restart count reached the max number of restarts allowed.' + } + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Restart not required as the monitored process was not in 'running' state ($($watchdog.ProcessInfo.Status))." + } + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'AutoRestart is disabled. Nothing to do.' + } + } + + # Ensure cleanup of resources + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Cleaning up resources...' + if ($null -ne $reader) { $reader.Dispose() } + if ($null -ne $pipeServer -and $pipeServer.IsConnected) { + $pipeServer.Disconnect() # Ensure server is disconnected + } + + # Release resources and reinitialize PipeServer + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Releasing resources and setting PipeServer to null.' + if ($null -ne $watchdog['PipeWriter']) { + $watchdog['PipeWriter'].Dispose() # Dispose existing PipeWriter + $watchdog['PipeWriter'] = $null + } + + if ($null -ne $pipeServer) { + $pipeServer.Dispose() # Dispose existing PipeServer + $watchdog['PipeServer'] = $null # Set to null for reinitialization + } + } + } + + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Stopping PipeServer...' + } +} + + +<# +.SYNOPSIS + Displays startup information for the Pode Watchdog service. + +.DESCRIPTION + This function outputs the status and details of the active Pode Watchdog service, including information about monitored processes. + It checks if the Watchdog service is enabled for the server, and if it is, prints a formatted table showing key process metrics such as memory usage, CPU usage, and process IDs. + This function helps in visually confirming that the Watchdog service is active and monitoring processes as expected. + +.PARAMETER None + This function does not take any parameters. + +.NOTES + This function is intended for internal use within the Pode Watchdog system to display startup messages. +#> + +function Write-PodeWatchdogStartupMessage { + # Check if the Watchdog service is enabled for the server + if ((Test-PodeWatchDogEnabled -Server)) { + + # Print a blank line and indicate that the Watchdog is active + Write-PodeHost + Write-PodeHost 'Watchdog [Active]' -ForegroundColor Cyan + + # If more than one process is monitored, adjust the message for plural + if ($PodeContext.Server.Watchdog.Server.Count -gt 1) { + Write-PodeHost 'Monitored processes:' -ForegroundColor Yellow + } + else { + Write-PodeHost 'Monitored process:' -ForegroundColor Yellow + } + + # Print the header for the process information table + Write-PodeHost "`tName`tNPM(K)`tPM(M)`tWS(M)`tCPU(s)`tId`tSI`tSBlock`tFile" -ForegroundColor Yellow + + # Loop through each monitored process in the Watchdog's server context + foreach ($name in $PodeContext.Server.Watchdog.Server.Keys) { + $watchdog = $PodeContext.Server.Watchdog.Server[$name] + $process = $watchdog.Process + $scriptblock = [string]::IsNullOrEmpty($watchdog.FilePath) + if ($scriptblock) { + $fileName = '' + } + else { + $fileName = Split-Path -Path $watchdog.FilePath -Leaf + } + + # Print process metrics: Name, NPM, PM, WS, CPU, Process Id, Session Id, ScriptBlock, FileName + Write-PodeHost "`t$name`t$($process.NPM)`t$([math]::round($process.PM / 1MB, 2))`t$([math]::round($process.WS / 1MB, 2))`t$([math]::round($process.CPU, 2))`t$($process.Id)`t$($process.SI)`t$scriptblock`t$filename" -ForegroundColor Yellow + } + } +} + + +<# +.SYNOPSIS + Temporary function in place to log messages. + +.DESCRIPTION + This function is used by the Watchdog script to log messages. It takes a single parameter, $Message, which is a string that contains the message to be logged. + +.OUTPUTS + [none] + +.NOTES + This function will be replace by a more robust one when https://github.com/Badgerati/Pode/pull/1387 will be merged. +#> + +function Write-PodeWatchdogLog { + [CmdletBinding(DefaultParameterSetName = 'Message')] + param( + [Parameter(Mandatory = $true)] + [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]] + $Watchdog, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')] + [System.Exception] + $Exception, + + [Parameter(ParameterSetName = 'Exception')] + [switch] + $CheckInnerException, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Error')] + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Message')] + [string] + $Message, + + [string] + $Level = 'Informational', + + [string] + $Tag = '-', + + [Parameter()] + [int] + $ThreadId + + ) + Process { + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + + 'message' { + $logItem = @{ + Name = $Watchdog.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Level = $Level + Message = $Message + Tag = $Tag + } + } + break + } + 'custom' { + $logItem = @{ + Name = $Watchdog.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Level = $Level + Message = $Message + Tag = $Tag + } + } + break + } + 'exception' { + $logItem = @{ + Name = $Watchdog.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Category = $Exception.Source + Message = $Exception.Message + StackTrace = $Exception.StackTrace + Level = $Level + } + } + Write-PodeErrorLog -Level $Level -CheckInnerException:$CheckInnerException -Exception $Exception + } + + 'error' { + $logItem = @{ + Name = $Watchdog.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Category = $ErrorRecord.CategoryInfo.ToString() + Message = $ErrorRecord.Exception.Message + StackTrace = $ErrorRecord.ScriptStackTrace + Level = $Level + } + } + Write-PodeErrorLog -Level $Level -ErrorRecord $ErrorRecord + } + } + + $lpath = Get-PodeRelativePath -Path './logs' -JoinRoot + $logItem | ConvertTo-Json -Compress -Depth 5 | Add-Content "$lpath/watchdog-$($Watchdog.Name).log" + + } +} diff --git a/src/Private/WatchdogHeartbeat.ps1 b/src/Private/WatchdogHeartbeat.ps1 new file mode 100644 index 000000000..0740b641a --- /dev/null +++ b/src/Private/WatchdogHeartbeat.ps1 @@ -0,0 +1,410 @@ +<# +.SYNOPSIS + Waits for active web sessions to close before proceeding with shutdown or restart. + +.DESCRIPTION + This function blocks all incoming requests by adding a middleware that responds with a 503 Service Unavailable status, along with a 'Retry-After' header to inform clients when to retry their requests. + It continuously checks for any active sessions and waits for them to finish. If the sessions do not close within the defined timeout period, the function exits and proceeds with the shutdown or restart process. + +.PARAMETER Timeout + The timeout is handled by `$PodeContext.Server.Watchdog.Client.ServiceUnavailableTimeout`, which defines the maximum time (in seconds) the function will wait for all sessions to close before exiting. + +.PARAMETER RetryAfter + The retry interval is managed by `$PodeContext.Server.Watchdog.Client.ServiceUnavailableRetryAfter`, which defines the value of the 'Retry-After' header (in seconds) that is sent in the 503 response. + +.EXAMPLE + Wait-PodeWatchdogSessionEnd + + Blocks new incoming requests, waits for active sessions to close, and exits when all sessions are closed or when the timeout is reached. + +.NOTES + This function is typically used during shutdown or restart operations in Pode to ensure that all active sessions are completed before the server is stopped or restarted. +#> +function Wait-PodeWatchdogSessionEnd { + try { + # Add middleware to block new requests and respond with 503 Service Unavailable + Enable-PodeWatchdogMonitored + + $previousOpenSessions = 0 + $startTime = [datetime]::Now + + write-PodeHost "Context count= $($PodeContext.Server.Signals.Listener.Contexts.Count)" + while ($PodeContext.Server.Signals.Listener.Contexts.Count -gt 0) { + if ($previousOpenSessions -ne $PodeContext.Server.Signals.Listener.Contexts.Count) { + Write-PodeHost "Waiting for the end of $($PodeContext.Server.Signals.Listener.Contexts.Count) sessions" + $previousOpenSessions = $PodeContext.Server.Signals.Listener.Contexts.Count + } + # Check if timeout is reached + if (([datetime]::Now - $startTime).TotalSeconds -ge $PodeContext.Server.Watchdog.Client.GracefulShutdownTimeout) { + Write-PodeHost "Timeout reached after $($PodeContext.Server.Watchdog.Client.GracefulShutdownTimeout) seconds, exiting..." + break + } + + Start-Sleep -Milliseconds 200 + } + } + catch { + Write-PodeHost $_ + } + finally { + # Remove middleware to block new requests and respond with 503 Service Unavailable + Disable-PodeWatchdogMonitored + } +} + +<# +.SYNOPSIS + Enables new requests by removing the middleware that blocks requests when the Pode Watchdog service is active. + +.DESCRIPTION + This function checks if the middleware associated with the Pode Watchdog client is present, and if so, it removes it to allow new requests. + This effectively re-enables access to the service by removing the request blocking. + +.NOTES + This function is used internally to manage Watchdog monitoring and may change in future releases of Pode. +#> +function Enable-PodeWatchdogMonitored { + $watchdog = $PodeContext.Server.Watchdog.Client + + # Check if the Watchdog middleware exists and remove it if found to allow new requests + if (Test-PodeMiddleware -Name $watchdog.PipeName) { + Remove-PodeMiddleware -Name $watchdog.PipeName + $watchdog.Accessible = $true + } +} + +<# +.SYNOPSIS + Disables new requests by adding middleware that blocks incoming requests when the Pode Watchdog service is active. + +.DESCRIPTION + This function adds middleware to the Pode server to block new incoming requests while the Pode Watchdog client is active. + It responds to all new requests with a 503 Service Unavailable status and sets a 'Retry-After' header, indicating when the service will be available again. + +.NOTES + This function is used internally to manage Watchdog monitoring and may change in future releases of Pode. +#> +function Disable-PodeWatchdogMonitored { + $watchdog = $PodeContext.Server.Watchdog.Client + + if (!(Test-PodeMiddleware -Name $watchdog.PipeName)) { + # Add middleware to block new requests and respond with 503 Service Unavailable + Add-PodeMiddleware -Name $watchdog.PipeName -ScriptBlock { + # Set HTTP response header for retrying after a certain time (RFC7231) + Set-PodeHeader -Name 'Retry-After' -Value $PodeContext.Server.Watchdog.Client.ServiceRecoveryTime + + # Set HTTP status to 503 Service Unavailable + Set-PodeResponseStatus -Code 503 + + # Stop further processing + return $false + } + $watchdog.Accessible = $false + } +} + +<# +.SYNOPSIS + Starts the Pode Watchdog client heartbeat and establishes communication with the server. + +.DESCRIPTION + This internal function initiates the Pode Watchdog client by connecting to the server via a named pipe, starting a timer for sending periodic updates, and handling commands such as shutdown and restart. + +.OUTPUTS + None + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Start-PodeWatchdogHearthbeat { + + # Define the script block that runs the client receiver and listens for commands from the server + $scriptBlock = { + Write-PodeHost 'Start client receiver' + $watchdog = $PodeContext.Server.Watchdog.Client + $watchdog['PipeReader'] = [System.IO.StreamReader]::new($watchdog.PipeClient) + $readTask = $null + while ($watchdog.Enabled) { + try { + if ($null -eq $readTask) { + # Asynchronously read data from the server without blocking + $readTask = $watchdog.PipeReader.ReadLineAsync() + } + + # Check if the read task has completed + if ($readTask.Status -eq [System.Threading.Tasks.TaskStatus]::RanToCompletion -and $readTask.Result) { + $serverMessage = $readTask.Result + + if ($serverMessage) { + Write-PodeHost "Received command from server: $serverMessage" + + # Handle server commands like shutdown and restart + switch ($serverMessage) { + 'shutdown' { + # Exit the loop and stop Pode Server + Write-PodeHost 'Server requested shutdown. Closing client...' + Set-PodeWatchdogHearthbeatStatus -Status 'Stopping' -Force + Wait-PodeWatchdogSessionEnd + Close-PodeServer + break + } + 'restart' { + # Exit the loop and restart Pode Server + Write-PodeHost 'Server requested restart. Restarting client...' + Set-PodeWatchdogHearthbeatStatus -Status 'Restarting' -Force + Wait-PodeWatchdogSessionEnd + Restart-PodeServer + break + } + 'disable' { + Write-PodeHost 'Server requested service to be refuse any new access.' + Disable-PodeWatchdogMonitored + break + } + 'enable' { + Write-PodeHost 'Server requested service to be enable new access.' + Enable-PodeWatchdogMonitored + break + } + + default { + Write-PodeHost "Unknown command received: $serverMessage" + } + } + } + # Reset the readTask after processing a message + $readTask = $null + } + elseif ($readTask.Status -eq [System.Threading.Tasks.TaskStatus]::Faulted) { + Write-PodeHost "Read operation faulted: $($readTask.Exception.Message)" + $readTask = $null # Reset in case of fault + } + Start-Sleep -Seconds 1 + } + catch { + Write-PodeHost "Error reading command from server: $_" + Start-Sleep -Seconds 1 # Sleep for a second before retrying in case of an error + } + } + } + + try { + # Initialize the Watchdog client and connect to the server's pipe + $watchdog = $PodeContext.Server.Watchdog.Client + $watchdog['PipeClient'] = [System.IO.Pipes.NamedPipeClientStream]::new('.', $watchdog.PipeName, [System.IO.Pipes.PipeDirection]::InOut, [System.IO.Pipes.PipeOptions]::Asynchronous) + $watchdog.PipeClient.Connect(60000) # Timeout of 60 seconds to connect + Write-PodeHost 'Connected to the watchdog pipe server.' + + # Create a persistent StreamWriter for writing messages to the server + $watchdog['PipeWriter'] = [System.IO.StreamWriter]::new($watchdog.PipeClient) + $watchdog['PipeWriter'].AutoFlush = $true # Enable auto-flush to send data immediately + $watchdog['Enabled'] = $true + $watchdog['Accessible'] = $true + + # Start the runspace for the client receiver + $watchdog['Runspace'] = Add-PodeRunspace -Type 'Watchdog' -ScriptBlock ($scriptBlock) -PassThru + + # Add a timer to send periodic updates to the server + Add-PodeTimer -Name '__pode_watchdog_client__' -Interval $watchdog.Interval -OnStart -ScriptBlock { + Send-PodeWatchdogData + } + } + catch [TimeoutException] { + # Handle timeout exceptions and close the server if connection fails + $_ | Write-PodeErrorLog + Close-PodeServer + } +} + +<# +.SYNOPSIS + Sends the current status and metrics of the Pode Watchdog client to the server. + +.DESCRIPTION + This function collects various metrics and status data from the Pode Watchdog client, such as uptime, restart count, and active listeners. + It serializes the data into JSON format and sends it to the server using a pipe. If the pipe connection is lost, the function attempts to reconnect and retry sending the data. + +.OUTPUTS + Sends serialized JSON data containing the current Watchdog status, uptime, restart count, metrics, and listeners. + +.NOTES + This function logs the data being sent and handles reconnection attempts if the pipe connection is lost. + +.EXAMPLE + Send-PodeWatchdogData + + Sends the current Watchdog status and metrics to the server and handles any connection issues. +#> +function Send-PodeWatchdogData { + $watchdog = $PodeContext.Server.Watchdog.Client + + # Collect and serialize Watchdog data to JSON format + $jsonData = [ordered]@{ + Status = $watchdog.Status + Accessible = $watchdog.Accessible + Pid = $PID + StartTime = $PodeContext.Metrics.Server.StartTime + InitialLoadTime = $PodeContext.Metrics.Server.InitialLoadTime + CurrentUptime = (Get-PodeServerUptime) + TotalUptime = (Get-PodeServerUptime -Total) + RestartCount = (Get-PodeServerRestartCount) + Listeners = $PodeContext.Server.Signals.Listener.Contexts + Requests = $PodeContext.Metrics.Requests + Signals = @{ + Total = $PodeContext.Metrics.Signals.Total + Server = $PodeContext.Server.Signals.Listener.ServerSignals + Client = $PodeContext.Server.Signals.Listener.ClientSignals + } + } | ConvertTo-Json -Compress -Depth 4 + + # Log and send the data to the server + Write-PodeHost "Sending watchdog data: $jsonData" + + try { + # Check if the pipe client is still connected before writing + if ($watchdog.PipeClient.IsConnected) { + # Write the serialized JSON data to the pipe + $watchdog.PipeWriter.WriteLine($jsonData) + } + else { + Write-PodeHost 'Pipe connection lost. Attempting to reconnect...' + Write-PodeLog -Name $name -InputObject 'Pipe connection lost. Attempting to reconnect...' + + # Attempt to reconnect to the pipe client + $watchdog.PipeClient.Connect(60000) # Retry connection + Write-PodeHost 'Reconnected to the watchdog pipe server.' + + # Retry sending the JSON data + $watchdog.PipeWriter.WriteLine($jsonData) + } + } + catch { + # Log errors and close the client on failure + $_ | Write-PodeErrorLog + Close-PodeServer + } +} + + + + +<# +.SYNOPSIS + Stops the Pode Watchdog client heartbeat and cleans up associated resources. + +.DESCRIPTION + This internal function stops the Pode Watchdog client by removing its timer, disabling it, and cleaning up resources such as the PipeClient, PipeReader, and PipeWriter. + +.OUTPUTS + None + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Stop-PodeWatchdogHearthbeat { + # Retrieve the Watchdog client from the Pode context + $watchdog = $PodeContext.Server.Watchdog.Client + + if ($PodeContext.Server.Watchdog.Client.Status -ne 'Restarting') { + # Watchdog client has stopped, updating status to 'Stopped' + Set-PodeWatchdogHearthbeatStatus -Status 'Stopped' + } + + # Remove the timer associated with the Watchdog client + Remove-PodeTimer -Name '__pode_watchdog_client__' + + + + # Send the last heartbeat to the watchdog server + Send-PodeWatchdogData + + # Disable the Watchdog client + $watchdog.Enabled = $false + + # Check if the PipeClient exists and clean up its resources + if ($null -ne $watchdog.PipeClient) { + + # Dispose of the PipeReader if it exists + if ($null -ne $watchdog.PipeReader) { + try { + $watchdog.PipeReader.Dispose() + } + catch { + $_ | Write-PodeErrorLog -Level Verbose # Log any errors during PipeReader disposal + } + } + + # Dispose of the PipeWriter if it exists + if ($null -ne $watchdog.PipeWriter) { + try { + $watchdog.PipeWriter.Dispose() + } + catch { + $_ | Write-PodeErrorLog -Level Verbose # Log any errors during PipeWriter disposal + } + } + + # Disconnect the PipeClient if it is still connected + if ($watchdog.PipeClient.IsConnected) { + $watchdog.PipeClient.Disconnect() + } + + # Dispose of the PipeClient itself + try { + $watchdog.PipeClient.Dispose() + } + catch { + $_ | Write-PodeErrorLog -Level Verbose # Log any errors during PipeClient disposal + } + } +} + +<# +.SYNOPSIS + Sets the status of the Pode Watchdog heartbeat for the client component. + +.DESCRIPTION + This function updates the status of the Pode Watchdog client heartbeat. It allows you to specify the current state of the Watchdog, such as 'Starting', 'Restarting', 'Running', 'Stopped', etc. + +.PARAMETER Status + Specifies the new status for the Pode Watchdog client heartbeat. Must be one of the following values: + - 'Starting' + - 'Restarting' + - 'Running' + - 'Undefined' + - 'Stopping' + - 'Stopped' + - 'Offline' + +.PARAMETER Force + Specifies whether to force to send the update of the Pode Watchdog client's heartbeat status to the server. + +.EXAMPLE + Set-PodeWatchdogHearthbeatStatus -Status 'Running' + + This command sets the Watchdog client's heartbeat status to 'Running'. + +.NOTES + This function checks if the Pode Watchdog client is enabled before updating the status. + This is an internal function and may change in future releases of Pode. +#> +function Set-PodeWatchdogHearthbeatStatus { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Starting', 'Restarting', 'Running', 'Undefined', 'Stopping', 'Stopped', 'Offline')] + [String] + $Status, + + [switch] + $Force + ) + + # Check if the Watchdog Client is enabled before updating the status + if ((Test-PodeWatchDogEnabled -Client)) { + $PodeContext.Server.Watchdog.Client.Status = $Status + if ($Force.IsPresent) { + Send-PodeWatchdogData + } + } +} + diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index ef83806eb..5836ebdb3 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -222,7 +222,9 @@ function Start-PodeServer { end { if ($pipelineItemCount -gt 1) { throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } # Store the name of the current runspace + } + + # Store the name of the current runspace $previousRunspaceName = Get-PodeCurrentRunspaceName # Sets the name of the current runspace Set-PodeCurrentRunspaceName -Name 'PodeServer' @@ -231,6 +233,26 @@ function Start-PodeServer { $Script:PodeContext = $null $ShowDoneMessage = $true + # check if podeWatchdog is configured + if ($PodeWatchdog) { + if ($null -ne $PodeWatchdog.DisableTermination -or + $null -ne $PodeWatchdog.Quiet -or + $null -ne $PodeWatchdog.PipeName -or + $null -ne $PodeWatchdog.Interval + ) { + if ($PodeWatchdog -is [hashtable]) { + $watchdogClient = ConvertTo-PodeConcurrentStructure -InputObject $PodeWatchdog + } + else { + $watchdogClient = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + $PodeWatchdog | Get-Member -MemberType Properties | ForEach-Object { + $watchdogClient[$_.Name] = $PodeWatchdog.$($_.Name) } + } + $DisableTermination = [switch]$watchdogClient.DisableTermination + $Quiet = [switch]$watchdogClient.Quiet + } + } + try { # if we have a filepath, resolve it - and extract a root path from it if ($PSCmdlet.ParameterSetName -ieq 'file') { @@ -268,6 +290,7 @@ function Start-PodeServer { EnableBreakpoints = $EnableBreakpoints IgnoreServerConfig = $IgnoreServerConfig ConfigFile = $ConfigFile + Watchdog = $watchdogClient Daemon = $Daemon } @@ -358,6 +381,7 @@ function Start-PodeServer { # clean the session $PodeContext = $null $PodeLocale = $null + $PodeWatchdog = $null } } } diff --git a/src/Public/Watchdog.ps1 b/src/Public/Watchdog.ps1 new file mode 100644 index 000000000..e341a51bc --- /dev/null +++ b/src/Public/Watchdog.ps1 @@ -0,0 +1,629 @@ +<# +.SYNOPSIS + Enables a Pode Watchdog service to monitor a script or file for changes and control its lifecycle. + +.DESCRIPTION + Configures and starts a Pode Watchdog service to monitor either a script block or a file path. + The Watchdog service can monitor files or directories for changes and automatically restart the monitored process when needed. + Additionally, it supports automatic process restarts based on a set of conditions, graceful shutdowns, and recovery times after restarts. + A hashtable of parameters can also be passed to the script block or file being monitored. + +.PARAMETER Name + The name of the Watchdog service. + +.PARAMETER ScriptBlock + The script block to be executed and monitored by the Watchdog service. + This parameter is mandatory when using the 'Script' or 'ScriptMonitoring' parameter sets. + +.PARAMETER FilePath + The path to the file to be executed and monitored by the Watchdog service. + This parameter is mandatory when using the 'File' or 'FileMonitoring' parameter sets. + +.PARAMETER Parameters + A hashtable of parameters to pass to the script block or file. + The keys of the hashtable represent the parameter names, and the values represent the parameter values. + These parameters are dynamically passed to the script block or file when the Watchdog invokes them. + +.PARAMETER FileMonitoring + Enables monitoring of a file or directory for changes. This can be used with either scripts or files. + +.PARAMETER FileExclude + An array of file patterns to exclude from the monitoring process. + For example: '*.log' to exclude all log files. + This is only applicable when 'FileMonitoring' is enabled. + +.PARAMETER FileInclude + An array of file patterns to include in the monitoring process. + Default is '*.*', which includes all files. + This is only applicable when 'FileMonitoring' is enabled. + +.PARAMETER MonitoredPath + The directory path to monitor for changes. + This parameter is mandatory when 'FileMonitoring' is enabled and can be used to define the root directory to watch. + +.PARAMETER Interval + The time interval, in seconds, for checking the Watchdog's state. + Default is 10 seconds. + +.PARAMETER NoAutostart + Disables the automatic restart of the monitored process when it stops or encounters an error. + +.PARAMETER RestartServiceAfter + Defines the time, in seconds, before the service attempts to restart the monitored process after a failure. + Default is 60 seconds. + +.PARAMETER MaxNumberOfRestarts + Specifies the maximum number of times the Watchdog is allowed to restart the monitored process in case of repeated failures. + Default is 5 restarts. + +.PARAMETER ResetFailCountAfter + The time, in minutes, after which the failure restart count will be reset if the process has been running continuously without issues. + Default is 5 minutes. + +.PARAMETER GracefulShutdownTimeout + Defines the maximum time, in seconds, the service waits for active sessions to close during a graceful shutdown. + If sessions remain open after this time, the service forces shutdown. + Default is 30 seconds. + +.PARAMETER ServiceRecoveryTime + Defines the time, in seconds, that the service indicates to clients when it will be available again after a restart. + This value is used in the 'Retry-After' header when responding with a 503 status. + Default is 60 seconds. + +.EXAMPLE + Enable-PodeWatchdog -FilePath $filePath -FileMonitoring -FileExclude '*.log' -Name 'MyWatch01' + + This example sets up a Watchdog named 'MyWatch01' to monitor changes in the specified file while excluding any log files from monitoring. + +.EXAMPLE + Enable-PodeWatchdog -ScriptBlock { Start-Sleep -Seconds 30 } -Name 'MyScriptWatchdog' -Parameter @{WaitTime=30} + + This example sets up a Watchdog to monitor a script block that sleeps for 30 seconds, passing the parameter 'WaitTime' with a value of 30 to the script block. + +.NOTES + Possible Monitored Process States: + - Restarting + - Starting + - Running + - Stopping + - Stopped + - Undefined +#> +function Enable-PodeWatchdog { + [CmdletBinding(DefaultParameterSetName = 'Script')] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Script')] + [Parameter(Mandatory = $true, ParameterSetName = 'ScriptMonitoring')] + [scriptblock] + $ScriptBlock, + + [Parameter(Mandatory = $true, ParameterSetName = 'File')] + [Parameter(Mandatory = $true, ParameterSetName = 'FileMonitoring')] + [string] + $FilePath, + + [Parameter(Mandatory = $false, ParameterSetName = 'File')] + [Parameter(Mandatory = $false, ParameterSetName = 'FileMonitoring')] + [hashtable] + $Parameters, + + [Parameter(Mandatory = $true, ParameterSetName = 'ScriptMonitoring')] + [Parameter(Mandatory = $true, ParameterSetName = 'FileMonitoring')] + [switch] + $FileMonitoring, + + [Parameter(Mandatory = $false, ParameterSetName = 'ScriptMonitoring')] + [Parameter(Mandatory = $false, ParameterSetName = 'FileMonitoring')] + [string[]] + $FileExclude, + + [Parameter(Mandatory = $false, ParameterSetName = 'ScriptMonitoring')] + [Parameter(Mandatory = $false, ParameterSetName = 'FileMonitoring')] + [ValidateNotNullOrEmpty()] + [string[]] + $FileInclude = '*.*', + + [Parameter(Mandatory = $false, ParameterSetName = 'FileMonitoring')] + [Parameter(Mandatory = $true, ParameterSetName = 'ScriptMonitoring')] + [string] + $MonitoredPath, + + [int] + $Interval = 10, + + [switch] + $NoAutostart, + + [int ] + $RestartServiceAfter = 60, + + [int] + $MaxNumberOfRestarts = 5, + + [int] + $ResetFailCountAfter = 5, + + [int] + $GracefulShutdownTimeout = 30, + + [int] + $ServiceRecoveryTime = 60 + ) + + if ($Parameters) { + # Convert the hashtable into a string with parameters + $parameterString = ($Parameters.GetEnumerator() | ForEach-Object { if ($_.Value -is [string]) { + # Escape single quotes by replacing ' with '' + $escapedValue = $_.Value -replace "'", "''" + "-$($_.Key) '$escapedValue'" + } + else { + "-$($_.Key) $($_.Value)" + } + }) -join ' ' + } + else { + $parameterString = '' + } + + # Check which parameter set is being used and adjust accordingly + if ($PSCmdlet.ParameterSetName -ieq 'File' -or $PSCmdlet.ParameterSetName -ieq 'FileMonitoring') { + # Resolve file path and determine root path + $FilePath = Get-PodeRelativePath -Path $FilePath -Resolve -TestPath -JoinRoot -RootPath $MyInvocation.PSScriptRoot + + # Construct arguments for file execution + $arguments = "-NoProfile -Command `"& { + `$global:PodeWatchdog = `$args[0] | ConvertFrom-Json; + . `"$FilePath`" $parameterString + }`"" + } + else { + # For 'Script' parameter set: serialize the scriptblock for execution + $scriptBlockString = $ScriptBlock.ToString() + $arguments = "-NoProfile -Command `"& { + `$PodeWatchdog = `$args[0] | ConvertFrom-Json; + & { $scriptBlockString } $parameterString + }`"" + } + + # Generate a unique PipeName for the Watchdog + $pipename = "$Name_$(New-PodeGuid)" + if ($null -eq $PodeContext.Server.Watchdog) { + $PodeContext.Server.Watchdog = @{ + Server = @{} + } + } + + # Create a hashtable for Watchdog configurations + $PodeWatchdog = @{ + DisableTermination = $true + Quiet = $true + PipeName = $pipename + Interval = $Interval + GracefulShutdownTimeout = $GracefulShutdownTimeout + ServiceRecoveryTime = $ServiceRecoveryTime + } + + # Serialize and escape the JSON configuration + $escapedJsonConfig = ($PodeWatchdog | ConvertTo-Json -Compress).Replace('"', '\"') + + # Initialize Watchdog context with parameters + $watchdog = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + $watchdog['Name'] = $Name + $watchdog['Shell'] = (Get-Process -Id $PID).Path + $watchdog['Arguments'] = "$arguments '$escapedJsonConfig'" + $watchdog['PipeName'] = $pipename + $watchdog['ScriptBlock'] = Get-PodeWatchdogPipeServerScriptBlock + $watchdog['Interval'] = $Interval + $watchdog['Enabled'] = $true + $watchdog['FilePath'] = $FilePath + $watchdog['RestartCount'] = -1 + $watchdog['AutoRestart'] = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new() + $watchdog.AutoRestart['Enabled'] = ! $NoAutostart.IsPresent + $watchdog.AutoRestart['RestartServiceAfter'] = $RestartServiceAfter + $watchdog.AutoRestart['MaxNumberOfRestarts'] = $MaxNumberOfRestarts + $watchdog.AutoRestart['ResetFailCountAfter'] = $ResetFailCountAfter * 60000 #in milliseconds + $watchdog.AutoRestart['RestartCount'] = 0 + + $watchdog['Runspace'] = $null + $watchdog['PipeServer'] = $null + $watchdog['PipeWriter'] = $null + $watchdog['ProcessInfo'] = [ordered]@{Status = 'Stopped'; Accessible = $false; Pid = '' } + $watchdog['Process'] = $null + + # Add Watchdog to the server context + $PodeContext.Server.Watchdog.Server[$Name] = $watchdog + + # Set up file monitoring if specified + if ($FileMonitoring.IsPresent) { + if ($MonitoredPath) { + if (Test-Path -Path $MonitoredPath -PathType Container) { + $path = $MonitoredPath + } + else { + throw ($PodeLocale.pathNotExistExceptionMessage -f $path) + } + } + else { + $path = (Get-Item $FilePath).DirectoryName + } + + Add-PodeFileWatcher -Path $path -Exclude $FileExclude -Include $FileInclude -ArgumentList $Name -ScriptBlock { + param($Name) + $watchdog = $PodeContext.Server.Watchdog.Server[$Name] + Write-PodeWatchdogLog -Watchdog $watchdog -Message "File [$($FileEvent.Type)]: $($FileEvent.FullPath) changed" + if (((Get-Date) - ($watchdog.Process.StartTime)).TotalMinutes -gt $watchdog.MinRestartInterval ) { + if ( $watchdog.FilePath -eq $FileEvent.FullPath) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Force a cold restart' + Set-PodeWatchdogProcessState -Name $Name -State ColdRestart + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'Force a restart' + Set-PodeWatchdogProcessState -Name $Name -State Restart + } + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message "Less than $($watchdog.MinRestartInterval) minutes are passed since last restart." + } + } + } +} + +<# +.SYNOPSIS + Checks if a Pode Watchdog service is enabled and running. + +.DESCRIPTION + Tests if a specified Watchdog service, identified by its name, is currently active and monitored by Pode. + If no name is specified, the function will check if any Watchdog client is active in the context. + +.PARAMETER Name + The name of the Watchdog service to check. + If not provided, the function will test for any active Watchdog clients. + +.OUTPUTS + Returns a boolean value indicating whether the specified Watchdog service (or any client) is active. + +.EXAMPLE + Test-PodeWatchdog -Name 'MyWatch01' + + This example checks if a Watchdog named 'MyWatch01' is active and running. +#> +function Test-PodeWatchdog { + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string] + $Name + ) + + # Return a boolean value based on the state of the Watchdog context + return ( + # Check if the Watchdog context is initialized + ($null -ne $PodeContext.Server.Watchdog) -and + ( + ( + # Check if a specific Watchdog service is being monitored + $PodeContext.Server.Watchdog.Server -and + (![string]::IsNullOrEmpty($Name)) -and + $PodeContext.Server.Watchdog.Server.ContainsKey($Name) + ) -or ( + # If no name is provided, check if any Watchdog client is active + ([string]::IsNullOrEmpty($Name)) -and + $PodeContext.Server.Watchdog.Client + ) + ) + ) +} +<# +.SYNOPSIS + Retrieves information about a monitored process managed by a Pode Watchdog. + +.DESCRIPTION + This function returns metrics and details regarding a monitored process that is being managed by a specified Pode Watchdog service. + The information can be filtered based on the provided type, such as the process status, request metrics, active listeners, or signal metrics. + +.PARAMETER Name + The name of the Watchdog service monitoring the process. + +.PARAMETER Type + Specifies the type of information to retrieve: + - 'Status': Returns the current status of the monitored process, such as Pid, Current Uptime, Total Uptime, and Restart Count. + - 'Requests': Returns metrics related to requests processed by the monitored process. + - 'Listeners': Returns the list of listeners active for the monitored process. + - 'Signals': Returns metrics related to signals processed by the monitored process. + If not specified, all available information regarding the monitored process will be returned. + +.OUTPUTS + Returns a hashtable containing the requested information about the monitored process. + +.EXAMPLE + Get-PodeWatchdogProcessMetric -Name 'MyWatch01' -Type 'Status' + + This example retrieves the current status of the monitored process managed by the Watchdog named 'MyWatch01', including its PID, uptime, and restart count. +#> +function Get-PodeWatchdogProcessMetric { + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $false)] + [ValidateSet('Status', 'Requests', 'Listeners', 'Signals')] + [string] $Type + ) + + # Check if the specified Watchdog service is active and managing a process + if ((Test-PodeWatchdog -Name $Name)) { + $watchdog = $PodeContext.Server.Watchdog.Server[$Name] + + # Ensure that process information is available for the monitored process + if ($null -ne $watchdog.ProcessInfo) { + $processInfo = $watchdog.ProcessInfo + + # Retrieve specific information based on the Type parameter + switch ($Type) { + 'Status' { + # Return a hashtable with status metrics about the monitored process + return @{ + Status = $processInfo.Status + Accessible = $processInfo.Accessible + Pid = $processInfo.Pid + CurrentUptime = $processInfo.CurrentUptime + TotalUptime = $processInfo.TotalUptime + RestartCount = $processInfo.RestartCount + InitialLoadTime = $processInfo.InitialLoadTime + StartTime = $processInfo.StartTime + } + } + 'Requests' { + # Return metrics related to requests handled by the monitored process + return $processInfo.Requests + } + 'Listeners' { + # Return a list of active listeners for the monitored process + return $processInfo.Listeners + } + 'Signals' { + # Return metrics related to signals processed by the monitored process + return $processInfo.Signals + } + default { + return $processInfo + } + } + } + else { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'ProcessInfo is empty' # Log that no process information is available for the monitored process + } + } + else { + # Log if the specified Watchdog is not monitoring any process + Write-PodeWatchdogLog -Watchdog $watchdog -Message "$Name is not a monitored process by any Watchdog" + } + + return $null +} + + +<# +.SYNOPSIS + Sets the state of a monitored process managed by a Pode Watchdog service. + +.DESCRIPTION + This function allows for controlling the execution state of a process being monitored by a Pode Watchdog service. + You can stop, start, restart, terminate, reset, disable, or enable the monitored process. The function checks if the specified Watchdog service is active and managing a process before performing the requested state change. + It integrates with the Watchdog's autorestart and communication features to ensure smooth state transitions. + +.PARAMETER Name + The name of the Watchdog service managing the monitored process. This parameter is mandatory. + +.PARAMETER State + Specifies the desired state for the monitored process: + - 'Stop': Stops the monitored process. + - 'Restart': Restarts the monitored process. + - 'Start': Starts the monitored process. + - 'Terminate': Forces the monitored process to stop. + - 'Reset': Stops and restarts the monitored process. + - 'Disable': Disables new requests by sending a command to the Watchdog. + - 'Enable': Enables new requests by sending a command to the Watchdog. + Default value is 'Stop'. + +.OUTPUTS + [boolean] + Returns a boolean value indicating whether the state change was successful. + +.EXAMPLE + Set-PodeWatchdogProcessState -Name 'MyWatch01' -State 'Restart' + + This example restarts the monitored process managed by the Watchdog named 'MyWatch01'. + +.EXAMPLE + Set-PodeWatchdogProcessState -Name 'MyWatch01' -State 'Disable' + + This example disables new requests for the monitored process managed by the Watchdog named 'MyWatch01', sending a 'disable' command. + +.NOTES + This function interacts with the Pode Watchdog services and may change in future releases. +#> +function Set-PodeWatchdogProcessState { + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $false)] + [ValidateSet('Stop', 'Restart', 'Start', 'Terminate', 'Reset', 'Disable', 'Enable')] + [string] + $State = 'Stop' + ) + + # Check if the specified Watchdog is active and managing a process + if ((Test-PodeWatchdog -Name $Name)) { + + # Change the state of the monitored process based on the specified $State value + switch ($State) { + 'Stop' { + # Stop the monitored process + return Stop-PodeWatchdogMonitoredProcess -Name $Name + } + 'Restart' { + # Restart the monitored process + return Restart-PodeWatchdogMonitoredProcess -Name $Name + } + 'Start' { + # Start the monitored process + return Start-PodeWatchdogMonitoredProcess -Name $Name + } + 'Terminate' { + # Force stop the monitored process + return Stop-PodeWatchdogMonitoredProcess -Name $Name -Force + } + 'Reset' { + # Reset the monitored process: stop, restart + if ((Stop-PodeWatchdogMonitoredProcess -Name $Name -Force)) { + return Start-PodeWatchdogMonitoredProcess -Name $Name + } + } + 'Disable' { + # Attempt to disable the service via pipe communication + return (Send-PodeWatchdogMessage -Name $Name -Command 'disable') + } + 'Enable' { + # Attempt to enable the service via pipe communication + return (Send-PodeWatchdogMessage -Name $Name -Command 'enable') + } + } + } + + # Return $false if the specified Watchdog or monitored process is not found + return $false +} + + +<# +.SYNOPSIS + Enables the AutoRestart feature for a specified Pode Watchdog service. + +.DESCRIPTION + This function enables the AutoRestart feature for the specified Watchdog service, ensuring that the service automatically restarts the monitored process if it stops. + +.PARAMETER Name + The name of the Watchdog service for which to enable AutoRestart. +#> +function Enable-PodeWatchdogAutoRestart { + param ( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # Check if the specified Watchdog service is active and managing a process + if ((Test-PodeWatchdog -Name $Name)) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'AutoRestart feature is Enabled' + $PodeContext.Server.Watchdog.Server[$Name].AutoRestart.Enabled = $true + } +} + + + +<# +.SYNOPSIS + Disables the AutoRestart feature for a specified Pode Watchdog service. + +.DESCRIPTION + This function disables the AutoRestart feature for the specified Watchdog service, preventing the service from automatically restarting the monitored process if it stops. + +.PARAMETER Name + The name of the Watchdog service for which to disable AutoRestart. +#> +function Disable-PodeWatchdogAutoRestart { + param ( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # Check if the specified Watchdog service is active and managing a process + if ((Test-PodeWatchdog -Name $Name)) { + Write-PodeWatchdogLog -Watchdog $watchdog -Message 'AutoRestart feature is Disabled' + $PodeContext.Server.Watchdog.Server[$Name].AutoRestart.Enabled = $false + } +} + + +<# +.SYNOPSIS + Adds OpenAPI component schemas related to Pode Watchdog metrics and status for OpenAPI documentation. + +.DESCRIPTION + This function generates various OpenAPI component schemas representing metrics and status for a monitored Pode process. + It creates schemas for requests, listeners, signals, and the overall status of the monitored process. These schemas + can be used in OpenAPI documentation to provide detailed information about the monitoring status and performance + of the Pode Watchdog service. + +.PARAMETER WatchdogSchemaPrefix + Specifies the base name for the component schemas. Default is 'PodeWatchdog'. + The generated schemas will have names prefixed with this base name, such as 'PodeWatchdogRequests' or 'PodeWatchdogStatus'. + +.EXAMPLE + Add-PodeWatchdogOASchema -WatchdogSchemaPrefix 'CustomWatchdog' + + This example adds OpenAPI schemas related to the Pode Watchdog service, with the base name 'CustomWatchdog'. + +.NOTES + This function is used internally for OpenAPI schema generation and may be subject to change in future versions. +#> + +function Add-PodeWatchdogOASchema { + param( + [string] + $WatchdogSchemaPrefix = 'PodeWatchdog' + ) + + # Add a schema for request metrics, including status codes and total requests. + New-PodeOAObjectProperty -Name 'StatusCode' -AdditionalProperties (New-PodeOAIntProperty -Description 'Count of responses for a specific status code') | + New-PodeOAIntProperty -Name 'Total' -Description 'Total number of requests' -Format Int32 -Required | + New-PodeOAObjectProperty -Example (@{'Total' = 6; StatusCode = @{ '200' = 1; '404' = 5 } }) | + Add-PodeOAComponentSchema -Name "$($WatchdogSchemaPrefix)Requests" -Description 'Request metrics' + + # Define listener metrics with properties for counts of listeners, queued requests, and processing requests. + $oaCount = New-PodeOAIntProperty -Name 'Count' -Description 'Number of listeners' -Format Int32 -Example 85 -Required | + New-PodeOAIntProperty -Name 'QueuedCount' -Description 'Number of queued requests' -Format Int32 -Example 60 -Required | + New-PodeOAIntProperty -Name 'ProcessingCount' -Description 'Number of requests being processed' -Format Int32 -Example 50 -Required + + # Add listener metrics as an OpenAPI component schema. + $oaCount | New-PodeOAObjectProperty | + Add-PodeOAComponentSchema -Name "$($WatchdogSchemaPrefix)Listeners" -Description 'Listener information for the monitored process' + + # Add a schema for signal metrics, including total signals and separate client/server listener properties. + New-PodeOAIntProperty -Name 'Total' -Description 'Total number of signals' -Format Int32 -Example 390 -Required | + New-PodeOAObjectProperty -Name 'Client' -properties $oaCount -Required | + New-PodeOAObjectProperty -Name 'Server' -properties $oaCount -Required | + New-PodeOAObjectProperty | + Add-PodeOAComponentSchema -Name "$($WatchdogSchemaPrefix)Signals" -Description 'Signal metrics' + + # Define status information for the monitored process, including its status, uptime, and restart count. + $oaStatus = New-PodeOAStringProperty -Name 'Status' -Description 'Monitored process status' -Enum 'Restarting', 'Starting', 'Running', 'Stopping', 'Stopped' -Example 'Running' -Required | + New-PodeOABoolProperty -Name 'Accessible' -Description 'Is the content on the monitored process accessible?' -Example $true -Required | + New-PodeOAIntProperty -Name 'Pid' -Description 'Process ID of the monitored process' -Format Int32 -Example 25412 -Required | + New-PodeOAIntProperty -Name 'CurrentUptime' -Description 'Current uptime of the monitored process in seconds' -Format Int32 -Example 17 -Required | + New-PodeOAIntProperty -Name 'TotalUptime' -Description 'Total uptime of the monitored process in seconds' -Format Int32 -Example 3816 -Required | + New-PodeOAIntProperty -Name 'RestartCount' -Description 'Number of times the process has been restarted' -Format Int32 -Example 2 -Required | + New-PodeOAStringProperty -Name 'InitialLoadTime' -Description 'Time the server was initially loaded' -Format 'date-time' -Example '2024-10-15T13:48:40.8797682Z' -Required | + New-PodeOAStringProperty -Name 'StartTime' -Description 'Time the server started' -Format 'date-time' -Example '2024-10-15T13:48:44.6778411Z' -Required + + # Add the status schema as an OpenAPI component. + $oaStatus | New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name "$($WatchdogSchemaPrefix)Status" -Description 'Status of the monitored process' + + # Combine the status schema with signals, requests, and listeners into a full monitoring schema. + $oaStatus | New-PodeOAComponentSchemaProperty -Name 'Signals' -Reference "$($WatchdogSchemaPrefix)Signals" -Required | + New-PodeOAComponentSchemaProperty -Name 'Requests' -Reference "$($WatchdogSchemaPrefix)Requests" -Required | + New-PodeOAComponentSchemaProperty -Name 'Listeners' -Reference "$($WatchdogSchemaPrefix)Listeners" -Required | + New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name "$($WatchdogSchemaPrefix)Monitor" -Description 'Process monitoring information' +} diff --git a/utility/PipeServer.ps1 b/utility/PipeServer.ps1 new file mode 100644 index 000000000..06d5ef97b --- /dev/null +++ b/utility/PipeServer.ps1 @@ -0,0 +1,68 @@ +param ( + [string]$PipeName = '28752_Watchdog' +) +# $global:PodeWatchdog = '{"PipeName":"28752_Watchdog","MonitoringPort":5051,"Type":"Client","Quiet":true,"EnableMonitoring":true,"Interval":10,"MonitoringAddress":"localhost","PreSharedKey":"a5eec3c8-1470-44f2-8c3e-caa267ce14b7","DisableTermination":true}' | ConvertFrom-Json; + +# Create a named pipe server stream with the specified pipe name +$pipeServer = [System.IO.Pipes.NamedPipeServerStream]::new( + $PipeName, + [System.IO.Pipes.PipeDirection]::InOut, + 1, + [System.IO.Pipes.PipeTransmissionMode]::Message, + [System.IO.Pipes.PipeOptions]::None +) + +# Informational output with Write-Verbose, only shown with -Verbose switch +Write-Verbose "Named Pipe Server started with pipe name '$PipeName'..." + +try { + while ($true) { + Write-Verbose 'Waiting for client connection...' + + # Wait for the client connection + $pipeServer.WaitForConnection() + Write-Verbose 'Client connected.' + + try { + # Create a StreamReader to read the incoming message from the connected client + $reader = [System.IO.StreamReader]::new($pipeServer) + + while ($pipeServer.IsConnected) { + # Read the next message, which contains the serialized hashtable + $receivedData = $reader.ReadLine() + + # Check if data was received + if ($receivedData) { + Write-Verbose "Received data: $receivedData" + + # Deserialize the received JSON string back into a hashtable + $hashtable = $receivedData | ConvertFrom-Json + Write-Verbose 'Received hashtable:' + Write-Host $hashtable | Format-List -Force # Keep this as Write-Host to display data regardless + } + else { + Write-Verbose 'No data received from client. Waiting for more data...' + } + } + Write-Verbose 'Client disconnected. Waiting for a new connection...' + } + catch { + Write-Host "Error reading from pipe: $_" + } + finally { + # Clean up after client disconnection + Write-Verbose 'Cleaning up resources...' + $reader.Dispose() + # Disconnect the pipe server to reset it for the next client + $pipeServer.Disconnect() + } + } +} +catch { + Write-Host "An unexpected error occurred: $_" +} +finally { + # Clean up + Write-Verbose 'Closing pipe server...' + $pipeServer.Dispose() +}