From 7ffe56c3191ff3c3e5e1707334c208a7baea2682 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 2 Nov 2024 15:45:16 -0700 Subject: [PATCH 01/34] Add Enhanced Diagnostic Dump Feature with Configurable Output Formats and Pipeline Support --- .gitignore | 3 + docs/Getting-Started/Debug.md | 87 ++++++++++++++ examples/server.psd1 | 5 + src/Pode.psd1 | 1 + src/Private/Context.ps1 | 26 +++-- src/Public/Core.ps1 | 6 + src/Public/Utilities.ps1 | 206 ++++++++++++++++++++++++++++++++++ 7 files changed, 327 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 079cb2f3a..6938c5f15 100644 --- a/.gitignore +++ b/.gitignore @@ -266,3 +266,6 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 docs/Getting-Started/Samples.md + +# Dump Folder +Dump diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index 0baef9a75..dd72fe5a7 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -246,3 +246,90 @@ Add-PodeSchedule -Name 'TestSchedule' -Cron '@hourly' -ScriptBlock { ``` In this example, the schedule outputs the name of the runspace executing the script block every hour. This can be useful for logging and monitoring purposes when dealing with multiple schedules or tasks. + + +## Memory Dump for Diagnostics + +Pode provides a powerful memory dump feature to capture detailed information during critical failures or fatal exceptions. This feature, triggered by the `Invoke-PodeDump` function, captures the state of your application, including memory usage, runspace details, variables, and stack traces. You can configure the dump format, enable or disable it, and specify the save location. By default, Pode saves the dump in JSON format, but you can also choose from other supported formats. + +### Configuring Dump Defaults + +To set up default options for the memory dump feature in Pode, you can configure them in the `server.psd1` configuration file. Under the `Server.Debug.Dump` section, you can specify whether to enable the dump, the default format, and the save path. Below is an example configuration for setting up defaults: + +```powershell +@{ + Server = @{ + Debug = @{ + Breakpoints = @{ + Enable = $true + } + Dump = @{ + Enable = $true + Format = 'Yaml' # Options: 'json', 'clixml', 'txt', 'bin', 'yaml' + Path = './Dump' # Path to save the dump files + } + } + } +} +``` + +- **Enable**: Boolean value to enable or disable the memory dump feature. +- **Format**: Specifies the default format for the dump file. Supported formats are `json`, `clixml`, `txt`, `bin`, and `yaml`. +- **Path**: Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. + +### Overriding Default Settings at Runtime + +The `Invoke-PodeDump` function allows you to override these defaults at runtime by passing parameters to specify the format and path. This can be useful for debugging in specific cases without altering the default configuration. + +```powershell +try { + # Simulate a critical error + throw [System.OutOfMemoryException] "Simulated out of memory error" +} +catch { + # Capture the dump with custom options + Invoke-PodeDump -ErrorRecord $_ -Format 'clixml' -Path 'C:\CustomDump' -Halt +} +``` + +In this example: +- The memory dump is saved in CLIXML format instead of the default. +- The dump file is saved in the specified directory (`C:\CustomDump`) instead of the default path. +- The `-Halt` switch will terminate the application after the dump is saved. + +### Using the Dump Feature in Pode + +To use the dump feature effectively in your Pode server, you may want to include it in specific places within your code to capture state information when critical errors occur. + +Example usage in a Pode server: + +```powershell +Start-PodeServer -EnableBreakpoints { + Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http + + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + try { + # Simulate an operation that could fail + $processes = Get-Process -ErrorAction Stop + Write-PodeJsonResponse -Value @{ Process = $processes[0] } + } + catch { + # Invoke a memory dump when a critical error occurs + Invoke-PodeDump -ErrorRecord $_ -Halt + } + } +} +``` + +In this setup, if an error occurs in the route, `Invoke-PodeDump` is called, capturing the current state and halting the application if the `-Halt` switch is set. + +### Dumping Options and Best Practices + +- **Enabling Dump in Production**: Enabling the dump in production can be valuable for diagnosing unexpected failures. However, use it selectively, especially if you are using binary or CLIXML formats, as they can produce large files. +- **Specifying Formats**: Choose a format based on your needs: + - **JSON** and **YAML** are human-readable and suitable for quick inspection. + - **CLIXML** is ideal for preserving object structures, especially when analyzing PowerShell-specific data. + - **Binary** is compact and suitable for raw state captures but requires deserialization for inspection. +- **Setting the Path**: Use a dedicated folder for dump files (e.g., `./Dump`) to keep diagnostic files organized. The default path in the configuration can be overridden at runtime if needed. + +With these configurations and usage practices, the memory dump feature in Pode can provide a powerful tool for diagnostics and debugging, capturing critical state information at the time of failure. \ No newline at end of file diff --git a/examples/server.psd1 b/examples/server.psd1 index d1858842e..64fc4a2c9 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -63,6 +63,11 @@ Breakpoints = @{ Enable = $true } + Dump = @{ + Enable = $true + Format = 'Yaml' + Path = './Dump' + } } } } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index ad02ac21c..34c63c00b 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -144,6 +144,7 @@ 'Get-PodeCurrentRunspaceName', 'Set-PodeCurrentRunspaceName', 'Invoke-PodeGC', + 'Invoke-PodeDump', # routes 'Add-PodeRoute', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 288d4f0ab..c685a54e7 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -190,6 +190,17 @@ function New-PodeContext { 'Errors' = 'errors' } + $ctx.Server.Debug = @{ + Breakpoints = @{ + Debug = $false + } + Dump = @{ + Enable = $true + Format = 'Json' + Path = './Dump' + } + } + # check if there is any global configuration $ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx @@ -214,10 +225,6 @@ function New-PodeContext { # debugging if ($EnableBreakpoints) { - if ($null -eq $ctx.Server.Debug) { - $ctx.Server.Debug = @{ Breakpoints = @{} } - } - $ctx.Server.Debug.Breakpoints.Enabled = $EnableBreakpoints.IsPresent } @@ -320,13 +327,13 @@ function New-PodeContext { # routes for pages and api $ctx.Server.Routes = [ordered]@{ -# common methods + # common methods 'get' = [ordered]@{} 'post' = [ordered]@{} 'put' = [ordered]@{} 'patch' = [ordered]@{} 'delete' = [ordered]@{} -# other methods + # other methods 'connect' = [ordered]@{} 'head' = [ordered]@{} 'merge' = [ordered]@{} @@ -900,7 +907,12 @@ function Set-PodeServerConfiguration { # debug $Context.Server.Debug = @{ Breakpoints = @{ - Enabled = [bool]$Configuration.Debug.Breakpoints.Enable + Enabled = [bool](Protect-PodeValue -Value $Configuration.Debug.Breakpoints.Enable -Default $Context.Server.Debug.Breakpoints.Enable) + } + Dump = @{ + Enable = [bool](Protect-PodeValue -Value $Configuration.Debug.Dump.Enable -Default $Context.Server.Debug.Dump.Enable) + Format = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Format -Default $Context.Server.Debug.Dump.Format) + Path = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Path -Default $Context.Server.Debug.Dump.Path) } } } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 3d609f763..298d4c05d 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -232,6 +232,12 @@ function Start-PodeServer { $PodeContext.Tokens.Cancellation.Cancel() } catch { + $_ | Write-PodeErrorLog + + if ($PodeContext.Server.Debug.Dump.Enable) { + Invoke-PodeDump -ErrorRecord $_ -Format $PodeContext.Server.Debug.Dump.Format -Path $PodeContext.Server.Debug.Dump.Path + } + Invoke-PodeEvent -Type Crash $ShowDoneMessage = $false throw diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 893ffd464..9fcee2773 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1486,3 +1486,209 @@ function Invoke-PodeGC { [System.GC]::Collect() } +<# +.SYNOPSIS + Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. + +.DESCRIPTION + The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and + variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within + the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process + after saving the dump. + +.PARAMETER ErrorRecord + The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. + Accepts input from the pipeline. + +.PARAMETER Format + Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. + +.PARAMETER Halt + Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. + +.PARAMETER Path + Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. + +.EXAMPLE + try { + # Simulate a critical error + throw [System.OutOfMemoryException] "Simulated out of memory error" + } + catch { + # Capture the dump in JSON format and halt the application + $_ | Invoke-PodeDump -Format 'json' -Halt + } + + This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. + +.EXAMPLE + try { + # Simulate a critical error + throw [System.AccessViolationException] "Simulated access violation error" + } + catch { + # Capture the dump in YAML format without halting + $_ | Invoke-PodeDump -Format 'yaml' + } + + This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. + +.NOTES + This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. + It may be further adapted to log additional details or support different formats for captured data. + +#> +function Invoke-PodeDump { + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + + [Parameter()] + [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] + [string] + $Format, + + [string] + $Path, + + [switch] + $Halt + ) + + # Begin block to handle pipeline input + begin { + # Default format and path from PodeContext + if ([string]::IsNullOrEmpty($Format)) { + $Format = $PodeContext.Server.Debug.Dump.Format + } + if ([string]::IsNullOrEmpty($Path)) { + $Path = $PodeContext.Server.Debug.Dump.Path + } + } + + # Process block to handle each pipeline input + process { + # Ensure Dump directory exists in the specified path + $dumpDirectory = (Get-PodeRelativePath -Path $Path -JoinRoot) + if (! (Test-Path -Path $dumpDirectory)) { + New-Item -ItemType Directory -Path $dumpDirectory | Out-Null + } + + # Capture process memory details + $process = Get-Process -Id $PID + $memoryDetails = @{ + ProcessId = $process.Id + ProcessName = $process.ProcessName + WorkingSet = $process.WorkingSet64 / 1MB + PrivateMemory = $process.PrivateMemorySize64 / 1MB + VirtualMemory = $process.VirtualMemorySize64 / 1MB + } + + # Capture the code causing the exception + $scriptContext = @{ + ScriptName = $ErrorRecord.InvocationInfo.ScriptName + Line = $ErrorRecord.InvocationInfo.Line + PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage + } + + # Capture stack trace information if available + $stackTrace = if ($ErrorRecord.Exception.StackTrace) { + $ErrorRecord.Exception.StackTrace -split "`n" + } + else { + 'No stack trace available' + } + + # Capture exception details + $exceptionDetails = @{ + ExceptionType = $ErrorRecord.Exception.GetType().FullName + Message = $ErrorRecord.Exception.Message + InnerException = $ErrorRecord.Exception.InnerException?.Message + } + + # Check if RunspacePools is not null before iterating + $runspacePoolDetails = @() + if ($null -ne $PodeContext.RunspacePools) { + foreach ($poolName in $PodeContext.RunspacePools.Keys) { + $pool = $PodeContext.RunspacePools[$poolName] + + # Check if pool is not null and has a valid runspace pool + if ($null -ne $pool -and $null -ne $pool.Pool) { + $runspaceVariables = @() + + # Check if the runspace pool is open + if ($pool.Pool.RunspacePoolStateInfo.State -eq 'Opened') { + # Access each runspace in the pool and capture variables + foreach ($runspace in $pool.Pool.GetRunspaces()) { + $variables = $runspace.SessionStateProxy.InvokeCommand.InvokeScript({ + Get-Variable | ForEach-Object { + @{ + Name = $_.Name + Value = try { $_.Value } catch { 'Error retrieving value' } + } + } + }) + $runspaceVariables += @{ + RunspaceId = $runspace.InstanceId + ThreadId = $runspace.GetExecutionContext().CurrentThread.ManagedThreadId + Variables = $variables + } + } + } + + $runspacePoolDetails += @{ + PoolName = $poolName + State = $pool.State + MaxThreads = $pool.Pool.MaxRunspaces + AvailableThreads = $pool.Pool.GetAvailableRunspaces() + RunspaceVariables = $runspaceVariables + } + } + } + } + + # Combine all captured information into a single object + $dumpInfo = @{ + Timestamp = (Get-Date).ToString('s') + Memory = $memoryDetails + ScriptContext = $scriptContext + StackTrace = $stackTrace + ExceptionDetails = $exceptionDetails + RunspacePools = $runspacePoolDetails + } + + # Determine file extension and save format based on selected Format + switch ($Format) { + 'json' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" + $dumpInfo | ConvertTo-Json -Depth 10 | Out-File -FilePath $dumpFilePath + } + 'clixml' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" + $dumpInfo | Export-Clixml -Path $dumpFilePath + } + 'txt' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" + $dumpInfo | Out-String | Out-File -FilePath $dumpFilePath + } + 'bin' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" + [System.IO.File]::WriteAllBytes($dumpFilePath, [System.Text.Encoding]::UTF8.GetBytes([System.Management.Automation.PSSerializer]::Serialize($dumpInfo, 1))) + } + 'yaml' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" + $dumpInfo | ConvertTo-PodeYaml | Out-File -FilePath $dumpFilePath + } + } + + Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" + + # If Halt switch is specified, close the application + if ($Halt) { + Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' + Stop-Process -Id $PID -Force + } + } +} + From 8cc7d02c5bb962d83b0723f3186c7ea441c530a8 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 2 Nov 2024 15:46:54 -0700 Subject: [PATCH 02/34] add pipe example --- docs/Getting-Started/Debug.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index dd72fe5a7..54bb14d49 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -246,7 +246,7 @@ Add-PodeSchedule -Name 'TestSchedule' -Cron '@hourly' -ScriptBlock { ``` In this example, the schedule outputs the name of the runspace executing the script block every hour. This can be useful for logging and monitoring purposes when dealing with multiple schedules or tasks. - + ## Memory Dump for Diagnostics @@ -315,7 +315,7 @@ Start-PodeServer -EnableBreakpoints { } catch { # Invoke a memory dump when a critical error occurs - Invoke-PodeDump -ErrorRecord $_ -Halt + $_ | Invoke-PodeDump -Halt } } } From 6121cb5080e8f1d31e40f821429162895faeee94 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 3 Nov 2024 09:36:28 -0800 Subject: [PATCH 03/34] various improvements --- examples/PetStore/Petstore-OpenApi.ps1 | 12 ++ src/Pode.psm1 | 4 + src/Private/Context.ps1 | 1 + src/Private/Helpers.ps1 | 281 +++++++++++++++++++++++++ src/Private/Server.ps1 | 3 + src/Public/Core.ps1 | 8 +- src/Public/Utilities.ps1 | 207 +----------------- 7 files changed, 317 insertions(+), 199 deletions(-) diff --git a/examples/PetStore/Petstore-OpenApi.ps1 b/examples/PetStore/Petstore-OpenApi.ps1 index ed78f7f96..bd50ca0c8 100644 --- a/examples/PetStore/Petstore-OpenApi.ps1 +++ b/examples/PetStore/Petstore-OpenApi.ps1 @@ -895,5 +895,17 @@ Some useful links: Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be deleted.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' + + Add-PodeRoute -PassThru -Method Get -Path '/dump' -ScriptBlock { + $format = $WebEvent.Query['format'] + try { + # Simulate a critical error + throw [System.DivideByZeroException] 'Simulated divide by zero error' + } + catch { + $_ | Invoke-PodeDump #-Halt -Format $format + } + } | Set-PodeOARouteInfo -Summary 'Dump state' -Description 'Dump the memory state of the server.' -Tags 'dump' -OperationId 'dump'-PassThru | + Set-PodeOARequest -Parameters (New-PodeOAStringProperty -Name 'format' -Description 'Dump export format.' -Enum 'json', 'clixml', 'txt', 'bin', 'yaml' -Default 'json' | ConvertTo-PodeOAParameter -In Query ) } } \ No newline at end of file diff --git a/src/Pode.psm1 b/src/Pode.psm1 index 3e2d95553..34a812c3a 100644 --- a/src/Pode.psm1 +++ b/src/Pode.psm1 @@ -139,4 +139,8 @@ try { catch { throw ("Failed to load the Pode module. $_") } +finally { + # Cleanup temporary variables + Remove-Variable -Name 'tmpPodeLocale', 'localesPath', 'moduleManifest', 'root', 'version', 'libsPath', 'netFolder', 'podeDll', 'sysfuncs', 'sysaliases', 'funcs', 'aliases', 'moduleManifestPath', 'moduleVersion' -ErrorAction SilentlyContinue +} diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index c685a54e7..3ae2c0cd3 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -415,6 +415,7 @@ function New-PodeContext { $ctx.Tokens = @{ Cancellation = [System.Threading.CancellationTokenSource]::new() Restart = [System.Threading.CancellationTokenSource]::new() + Dump = [System.Threading.CancellationTokenSource]::new() } # requests that should be logged diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 8e4e9f37d..6fbc65d6c 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3766,4 +3766,285 @@ function Copy-PodeObjectDeepClone { # Deserialize the XML back into a new PSObject, creating a deep clone of the original return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) } +} + + + + +# Function to collect variables by scope +function Get-ScopedVariable { + param () + # Safeguard against deeply nested objects + function ConvertTo-SerializableObject { + param ( + [object]$InputObject, + [int]$MaxDepth = 5, # Default max depth + [int]$CurrentDepth = 0 + ) + + if ($CurrentDepth -ge $MaxDepth) { + return 'Max depth reached' + } + + if ($InputObject -is [System.Collections.ListDictionaryInternal] -or $InputObject -is [hashtable]) { + $result = @{} + foreach ($key in $InputObject.Keys) { + $strKey = $key.ToString() + $result[$strKey] = ConvertTo-SerializableObject -InputObject $InputObject[$key] -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + return $result + } + elseif ($InputObject -is [PSCustomObject]) { + $result = @{} + foreach ($property in $InputObject.PSObject.Properties) { + $result[$property.Name.ToString()] = ConvertTo-SerializableObject -InputObject $property.Value -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + return $result + } + elseif ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { + return $InputObject | ForEach-Object { ConvertTo-SerializableObject -InputObject $_ -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) } + } + else { + return $InputObject + } + } + + $scopes = @{ + Local = Get-Variable -Scope 0 + Script = Get-Variable -Scope Script + Global = Get-Variable -Scope Global + } + + $scopedVariables = @{} + foreach ($scope in $scopes.Keys) { + $variables = @{} + foreach ($var in $scopes[$scope]) { + $variables[$var.Name] = try { $var.Value } catch { 'Error retrieving value' } + } + $scopedVariables[$scope] = ConvertTo-SerializableObject -InputObject $variables -MaxDepth $MaxDepth + } + return $scopedVariables +} + + + +<# +.SYNOPSIS + Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. + +.DESCRIPTION + The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and + variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within + the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process + after saving the dump. + +.PARAMETER ErrorRecord + The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. + Accepts input from the pipeline. + +.PARAMETER Format + Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. + +.PARAMETER Halt + Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. + +.PARAMETER Path + Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. + +.EXAMPLE + try { + # Simulate a critical error + throw [System.OutOfMemoryException] "Simulated out of memory error" + } + catch { + # Capture the dump in JSON format and halt the application + $_ | Invoke-PodeDump -Format 'json' -Halt + } + + This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. + +.EXAMPLE + try { + # Simulate a critical error + throw [System.AccessViolationException] "Simulated access violation error" + } + catch { + # Capture the dump in YAML format without halting + $_ | Invoke-PodeDump -Format 'yaml' + } + + This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. + +.NOTES + This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. + It may be further adapted to log additional details or support different formats for captured data. + +#> +function Invoke-PodeDumpInternal { + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + + [Parameter()] + [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] + [string] + $Format, + + [string] + $Path, + + [switch] + $Halt, + + [int] + $MaxDepth = 5 + ) + + # Begin block to handle pipeline input + begin { + # Default format and path from PodeContext + if ([string]::IsNullOrEmpty($Format)) { + $Format = $PodeContext.Server.Debug.Dump.Format + } + if ([string]::IsNullOrEmpty($Path)) { + $Path = $PodeContext.Server.Debug.Dump.Path + } + if ($null -eq $ErrorRecord) { + $ErrorRecord = [System.Management.Automation.ErrorRecord]::new() + } + } + + # Process block to handle each pipeline input + process { + # Ensure Dump directory exists in the specified path + $dumpDirectory = (Get-PodeRelativePath -Path $Path -JoinRoot) + if (!(Test-Path -Path $dumpDirectory)) { + New-Item -ItemType Directory -Path $dumpDirectory | Out-Null + } + + # Capture process memory details + $process = Get-Process -Id $PID + $memoryDetails = @( + [Ordered]@{ + ProcessId = $process.Id + ProcessName = $process.ProcessName + WorkingSet = [math]::Round($process.WorkingSet64 / 1MB, 2) + PrivateMemory = [math]::Round($process.PrivateMemorySize64 / 1MB, 2) + VirtualMemory = [math]::Round($process.VirtualMemorySize64 / 1MB, 2) + } + ) + + # Capture the code causing the exception + $scriptContext = @( + [Ordered]@{ + ScriptName = $ErrorRecord.InvocationInfo.ScriptName + Line = $ErrorRecord.InvocationInfo.Line + PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage + } + ) + + # Capture stack trace information if available + $stackTrace = if ($ErrorRecord.Exception.StackTrace) { + $ErrorRecord.Exception.StackTrace -split "`n" + } + else { + 'No stack trace available' + } + + # Capture exception details + $exceptionDetails = @( + [Ordered]@{ + ExceptionType = $ErrorRecord.Exception.GetType().FullName + Message = $ErrorRecord.Exception.Message + InnerException = if ($ErrorRecord.Exception.InnerException) { $ErrorRecord.Exception.InnerException.Message } else { $null } + } + ) + + # Collect variables by scope + $scopedVariables = Get-ScopedVariable + + # Check if RunspacePools is not null before iterating + $runspacePoolDetails = @() + if ($null -ne $PodeContext.RunspacePools) { + write-podehost $PodeContext -explode -showtype + write-podehost 'sss' + foreach ($poolName in $PodeContext.RunspacePools.Keys) { + $pool = $PodeContext.RunspacePools[$poolName] + + if ($null -ne $pool -and $null -ne $pool.Pool) { + $runspaceVariables = @() + + if ($pool.Pool.RunspacePoolStateInfo.State -eq 'Opened') { + # write-podehost $pool.Pool -Explode -ShowType + foreach ($runspace in $pool.Pool.GetRunspaces()) { + $variables = $runspace.SessionStateProxy.InvokeCommand.InvokeScript({ + Get-ScopedVariable + }) + $runspaceVariables += @( + [Ordered]@{ + RunspaceId = $runspace.InstanceId + ThreadId = $runspace.GetExecutionContext().CurrentThread.ManagedThreadId + Variables = $variables + } + ) + } + } + + $runspacePoolDetails += @( + [Ordered]@{ + PoolName = $poolName + State = $pool.State + MaxThreads = $pool.Pool.MaxRunspaces + AvailableThreads = $pool.Pool.GetAvailableRunspaces() + RunspaceVariables = $runspaceVariables + } + ) + } + } + } + + # Combine all captured information into a single object + $dumpInfo = [Ordered]@{ + Timestamp = (Get-Date).ToString('s') + Memory = $memoryDetails + ScriptContext = $scriptContext + StackTrace = $stackTrace + ExceptionDetails = $exceptionDetails + ScopedVariables = $scopedVariables + RunspacePools = $runspacePoolDetails + } + + # Determine file extension and save format based on selected Format + switch ($Format) { + 'json' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" + $dumpInfo | ConvertTo-Json -Depth $MaxDepth | Out-File -FilePath $dumpFilePath + } + 'clixml' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" + $dumpInfo | Export-Clixml -Path $dumpFilePath + } + 'txt' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" + $dumpInfo | Out-String | Out-File -FilePath $dumpFilePath + } + 'bin' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" + [System.IO.File]::WriteAllBytes($dumpFilePath, [System.Text.Encoding]::UTF8.GetBytes([System.Management.Automation.PSSerializer]::Serialize($dumpInfo, 1))) + } + 'yaml' { + $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" + $dumpInfo | ConvertTo-PodeYaml | Out-File -FilePath $dumpFilePath + } + } + + Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" + + # If Halt switch is specified, close the application + if ($Halt) { + Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' + Stop-Process -Id $PID -Force + } + } } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 2472c1f91..1ffc30920 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -335,6 +335,9 @@ function Restart-PodeInternalServer { Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart $PodeContext.Tokens.Restart = [System.Threading.CancellationTokenSource]::new() + Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump + $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() + # reload the configuration $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 298d4c05d..45e48e890 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -147,7 +147,7 @@ function Start-PodeServer { Set-PodeCurrentRunspaceName -Name 'PodeServer' # ensure the session is clean - $PodeContext = $null + $Script:PodeContext = $null $ShowDoneMessage = $true try { @@ -214,6 +214,10 @@ function Start-PodeServer { Restart-PodeInternalServer } + if (($PodeContext.Tokens.Dump.IsCancellationRequested) ) { + Invoke-PodeDumpInternal -Halt + } + # check for open browser if (Test-PodeOpenBrowserPressed -Key $key) { Invoke-PodeEvent -Type Browser @@ -235,7 +239,7 @@ function Start-PodeServer { $_ | Write-PodeErrorLog if ($PodeContext.Server.Debug.Dump.Enable) { - Invoke-PodeDump -ErrorRecord $_ -Format $PodeContext.Server.Debug.Dump.Format -Path $PodeContext.Server.Debug.Dump.Path + Invoke-PodeDumpInternal -ErrorRecord $_ } Invoke-PodeEvent -Type Crash diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 9fcee2773..568cca664 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1486,209 +1486,22 @@ function Invoke-PodeGC { [System.GC]::Collect() } -<# -.SYNOPSIS - Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. - -.DESCRIPTION - The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and - variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within - the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process - after saving the dump. - -.PARAMETER ErrorRecord - The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. - Accepts input from the pipeline. -.PARAMETER Format - Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. -.PARAMETER Halt - Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. -.PARAMETER Path - Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. - -.EXAMPLE - try { - # Simulate a critical error - throw [System.OutOfMemoryException] "Simulated out of memory error" - } - catch { - # Capture the dump in JSON format and halt the application - $_ | Invoke-PodeDump -Format 'json' -Halt - } +<# +.SYNOPSIS +Invokes the Dump. - This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. +.DESCRIPTION +Invokes the Dump. .EXAMPLE - try { - # Simulate a critical error - throw [System.AccessViolationException] "Simulated access violation error" - } - catch { - # Capture the dump in YAML format without halting - $_ | Invoke-PodeDump -Format 'yaml' - } - - This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. - -.NOTES - This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. - It may be further adapted to log additional details or support different formats for captured data. - +Invoke-PodeDump #> function Invoke-PodeDump { - param ( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [System.Management.Automation.ErrorRecord] - $ErrorRecord, - - [Parameter()] - [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] - [string] - $Format, - - [string] - $Path, - - [switch] - $Halt - ) - - # Begin block to handle pipeline input - begin { - # Default format and path from PodeContext - if ([string]::IsNullOrEmpty($Format)) { - $Format = $PodeContext.Server.Debug.Dump.Format - } - if ([string]::IsNullOrEmpty($Path)) { - $Path = $PodeContext.Server.Debug.Dump.Path - } - } - - # Process block to handle each pipeline input - process { - # Ensure Dump directory exists in the specified path - $dumpDirectory = (Get-PodeRelativePath -Path $Path -JoinRoot) - if (! (Test-Path -Path $dumpDirectory)) { - New-Item -ItemType Directory -Path $dumpDirectory | Out-Null - } - - # Capture process memory details - $process = Get-Process -Id $PID - $memoryDetails = @{ - ProcessId = $process.Id - ProcessName = $process.ProcessName - WorkingSet = $process.WorkingSet64 / 1MB - PrivateMemory = $process.PrivateMemorySize64 / 1MB - VirtualMemory = $process.VirtualMemorySize64 / 1MB - } - - # Capture the code causing the exception - $scriptContext = @{ - ScriptName = $ErrorRecord.InvocationInfo.ScriptName - Line = $ErrorRecord.InvocationInfo.Line - PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage - } - - # Capture stack trace information if available - $stackTrace = if ($ErrorRecord.Exception.StackTrace) { - $ErrorRecord.Exception.StackTrace -split "`n" - } - else { - 'No stack trace available' - } - - # Capture exception details - $exceptionDetails = @{ - ExceptionType = $ErrorRecord.Exception.GetType().FullName - Message = $ErrorRecord.Exception.Message - InnerException = $ErrorRecord.Exception.InnerException?.Message - } - - # Check if RunspacePools is not null before iterating - $runspacePoolDetails = @() - if ($null -ne $PodeContext.RunspacePools) { - foreach ($poolName in $PodeContext.RunspacePools.Keys) { - $pool = $PodeContext.RunspacePools[$poolName] - - # Check if pool is not null and has a valid runspace pool - if ($null -ne $pool -and $null -ne $pool.Pool) { - $runspaceVariables = @() - - # Check if the runspace pool is open - if ($pool.Pool.RunspacePoolStateInfo.State -eq 'Opened') { - # Access each runspace in the pool and capture variables - foreach ($runspace in $pool.Pool.GetRunspaces()) { - $variables = $runspace.SessionStateProxy.InvokeCommand.InvokeScript({ - Get-Variable | ForEach-Object { - @{ - Name = $_.Name - Value = try { $_.Value } catch { 'Error retrieving value' } - } - } - }) - $runspaceVariables += @{ - RunspaceId = $runspace.InstanceId - ThreadId = $runspace.GetExecutionContext().CurrentThread.ManagedThreadId - Variables = $variables - } - } - } - - $runspacePoolDetails += @{ - PoolName = $poolName - State = $pool.State - MaxThreads = $pool.Pool.MaxRunspaces - AvailableThreads = $pool.Pool.GetAvailableRunspaces() - RunspaceVariables = $runspaceVariables - } - } - } - } - - # Combine all captured information into a single object - $dumpInfo = @{ - Timestamp = (Get-Date).ToString('s') - Memory = $memoryDetails - ScriptContext = $scriptContext - StackTrace = $stackTrace - ExceptionDetails = $exceptionDetails - RunspacePools = $runspacePoolDetails - } - - # Determine file extension and save format based on selected Format - switch ($Format) { - 'json' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" - $dumpInfo | ConvertTo-Json -Depth 10 | Out-File -FilePath $dumpFilePath - } - 'clixml' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" - $dumpInfo | Export-Clixml -Path $dumpFilePath - } - 'txt' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" - $dumpInfo | Out-String | Out-File -FilePath $dumpFilePath - } - 'bin' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" - [System.IO.File]::WriteAllBytes($dumpFilePath, [System.Text.Encoding]::UTF8.GetBytes([System.Management.Automation.PSSerializer]::Serialize($dumpInfo, 1))) - } - 'yaml' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" - $dumpInfo | ConvertTo-PodeYaml | Out-File -FilePath $dumpFilePath - } - } - - Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" - - # If Halt switch is specified, close the application - if ($Halt) { - Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' - Stop-Process -Id $PID -Force - } - } -} + [CmdletBinding()] + param( ) + $PodeContext.Tokens.Dump.Cancel() +} \ No newline at end of file From dbf54d9d69d69bc1e67d640e58bb54f70d93b4be Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 3 Nov 2024 20:14:16 -0800 Subject: [PATCH 04/34] Fixes and improvements --- examples/PetStore/Petstore-OpenApi.ps1 | 2 +- examples/PetStore/data/PetData.json | 8 + examples/PetStore/server.psd1 | 12 ++ examples/server.psd1 | 7 +- src/Private/Context.ps1 | 18 ++- src/Private/Helpers.ps1 | 195 ++++++++++++++++--------- src/Private/Server.ps1 | 80 +++++----- src/Public/Core.ps1 | 10 +- src/Public/Utilities.ps1 | 72 ++++++++- 9 files changed, 285 insertions(+), 119 deletions(-) diff --git a/examples/PetStore/Petstore-OpenApi.ps1 b/examples/PetStore/Petstore-OpenApi.ps1 index bd50ca0c8..7a298cdae 100644 --- a/examples/PetStore/Petstore-OpenApi.ps1 +++ b/examples/PetStore/Petstore-OpenApi.ps1 @@ -903,7 +903,7 @@ Some useful links: throw [System.DivideByZeroException] 'Simulated divide by zero error' } catch { - $_ | Invoke-PodeDump #-Halt -Format $format + $_ | Invoke-PodeDump -Format $format } } | Set-PodeOARouteInfo -Summary 'Dump state' -Description 'Dump the memory state of the server.' -Tags 'dump' -OperationId 'dump'-PassThru | Set-PodeOARequest -Parameters (New-PodeOAStringProperty -Name 'format' -Description 'Dump export format.' -Enum 'json', 'clixml', 'txt', 'bin', 'yaml' -Default 'json' | ConvertTo-PodeOAParameter -In Query ) diff --git a/examples/PetStore/data/PetData.json b/examples/PetStore/data/PetData.json index dfee685c6..0f8f81e79 100644 --- a/examples/PetStore/data/PetData.json +++ b/examples/PetStore/data/PetData.json @@ -43,6 +43,14 @@ "petId": 1, "id": 2, "quantity": 50 + }, + "10": { + "id": 10, + "petId": 198772, + "quantity": 7, + "shipDate": "2024-11-04T02:08:54.351Z", + "status": "approved", + "complete": true } }, "Scope": [ diff --git a/examples/PetStore/server.psd1 b/examples/PetStore/server.psd1 index de82d77dc..e58ac0356 100644 --- a/examples/PetStore/server.psd1 +++ b/examples/PetStore/server.psd1 @@ -10,10 +10,22 @@ Server = @{ Timeout = 60 BodySize = 100MB + Debug = @{ + Breakpoints = @{ + Enable = $true + } + Dump = @{ + Enabled = $true + Format = 'json' + Path = './Dump' + MaxDepth = 6 + } + } } Web = @{ OpenApi = @{ DefaultDefinitionTag = 'v3.0.3' } } + } \ No newline at end of file diff --git a/examples/server.psd1 b/examples/server.psd1 index 64fc4a2c9..07a5d3513 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -64,9 +64,10 @@ Enable = $true } Dump = @{ - Enable = $true - Format = 'Yaml' - Path = './Dump' + Enabled = $true + Format = 'json' + Path = './Dump' + MaxDepth = 6 } } } diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 3ae2c0cd3..aa5ab6d28 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -195,9 +195,11 @@ function New-PodeContext { Debug = $false } Dump = @{ - Enable = $true - Format = 'Json' - Path = './Dump' + Enabled = $true + Format = 'Json' + Path = './Dump' + MaxDepth = 5 + Param = @{} } } @@ -415,7 +417,7 @@ function New-PodeContext { $ctx.Tokens = @{ Cancellation = [System.Threading.CancellationTokenSource]::new() Restart = [System.Threading.CancellationTokenSource]::new() - Dump = [System.Threading.CancellationTokenSource]::new() + Dump = [System.Threading.CancellationTokenSource]::new() } # requests that should be logged @@ -911,9 +913,11 @@ function Set-PodeServerConfiguration { Enabled = [bool](Protect-PodeValue -Value $Configuration.Debug.Breakpoints.Enable -Default $Context.Server.Debug.Breakpoints.Enable) } Dump = @{ - Enable = [bool](Protect-PodeValue -Value $Configuration.Debug.Dump.Enable -Default $Context.Server.Debug.Dump.Enable) - Format = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Format -Default $Context.Server.Debug.Dump.Format) - Path = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Path -Default $Context.Server.Debug.Dump.Path) + Enabled = [bool](Protect-PodeValue -Value $Configuration.Debug.Dump.Enabled -Default $Context.Server.Debug.Dump.Enabled) + Format = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Format -Default $Context.Server.Debug.Dump.Format) + Path = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Path -Default $Context.Server.Debug.Dump.Path) + MaxDepth = [int] (Protect-PodeValue -Value $Configuration.Debug.Dump.MaxDepth -Default $Context.Server.Debug.Dump.MaxDepth) + Param = @{} } } } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 6fbc65d6c..58c90bca6 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -591,6 +591,15 @@ function Test-PodeOpenBrowserPressed { return (Test-PodeKeyPressed -Key $Key -Character 'b') } +function Test-PodeDumpPressed { + param( + [Parameter()] + $Key = $null + ) + + return (Test-PodeKeyPressed -Key $Key -Character 'd') +} + function Test-PodeKeyPressed { param( [Parameter()] @@ -3772,7 +3781,7 @@ function Copy-PodeObjectDeepClone { # Function to collect variables by scope -function Get-ScopedVariable { +function Get-PodeDumpScopedVariable { param () # Safeguard against deeply nested objects function ConvertTo-SerializableObject { @@ -3786,26 +3795,49 @@ function Get-ScopedVariable { return 'Max depth reached' } - if ($InputObject -is [System.Collections.ListDictionaryInternal] -or $InputObject -is [hashtable]) { + if ($null -eq $InputObject ) { + return $null + } + elseif ($InputObject -is [System.Collections.ListDictionaryInternal] -or $InputObject -is [hashtable]) { $result = @{} - foreach ($key in $InputObject.Keys) { - $strKey = $key.ToString() - $result[$strKey] = ConvertTo-SerializableObject -InputObject $InputObject[$key] -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + try { + foreach ($key in $InputObject.Keys) { + try { + $strKey = $key.ToString() + $result[$strKey] = ConvertTo-SerializableObject -InputObject $InputObject[$key] -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + catch { + write-podehost $_ -ForegroundColor Red + } + } + } + catch { + write-podehost $_ -ForegroundColor Red } return $result } elseif ($InputObject -is [PSCustomObject]) { $result = @{} - foreach ($property in $InputObject.PSObject.Properties) { - $result[$property.Name.ToString()] = ConvertTo-SerializableObject -InputObject $property.Value -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + try { + foreach ($property in $InputObject.PSObject.Properties) { + try { + $result[$property.Name.ToString()] = ConvertTo-SerializableObject -InputObject $property.Value -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + catch { + write-podehost $_ -ForegroundColor Red + } + } + } + catch { + write-podehost $_ -ForegroundColor Red } return $result } elseif ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { return $InputObject | ForEach-Object { ConvertTo-SerializableObject -InputObject $_ -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) } } - else { - return $InputObject + else { + return $InputObject.ToString() } } @@ -3894,33 +3926,61 @@ function Invoke-PodeDumpInternal { [string] $Path, - [switch] - $Halt, - [int] - $MaxDepth = 5 + $MaxDepth ) # Begin block to handle pipeline input begin { + # Default format and path from PodeContext if ([string]::IsNullOrEmpty($Format)) { - $Format = $PodeContext.Server.Debug.Dump.Format + if ($PodeContext.Server.Debug.Dump.Param.Format) { + $Format = $PodeContext.Server.Debug.Dump.Param.Format + } + else { + $Format = $PodeContext.Server.Debug.Dump.Format + } } if ([string]::IsNullOrEmpty($Path)) { - $Path = $PodeContext.Server.Debug.Dump.Path + if ($PodeContext.Server.Debug.Dump.Param.Path) { + $Path = $PodeContext.Server.Debug.Dump.Param.Path + } + else { + $Path = $PodeContext.Server.Debug.Dump.Path + } } if ($null -eq $ErrorRecord) { - $ErrorRecord = [System.Management.Automation.ErrorRecord]::new() + if ($PodeContext.Server.Debug.Dump.Param.ErrorRecord) { + $ErrorRecord = $PodeContext.Server.Debug.Dump.Param.ErrorRecord + } + else { + $ErrorRecord = $null + } } + + if ($MaxDepth -lt 1) { + if ($PodeContext.Server.Debug.Dump.Param.MaxDepth) { + $MaxDepth = $PodeContext.Server.Debug.Dump.Param.MaxDepth + } + else { + $MaxDepth = $PodeContext.Server.Debug.Dump.MaxDepth + } + } + $PodeContext.Server.Debug.Dump.Param.Clear() + + Write-PodeHost -ForegroundColor Yellow 'Preparing Memory Dump ...' } # Process block to handle each pipeline input process { # Ensure Dump directory exists in the specified path - $dumpDirectory = (Get-PodeRelativePath -Path $Path -JoinRoot) - if (!(Test-Path -Path $dumpDirectory)) { - New-Item -ItemType Directory -Path $dumpDirectory | Out-Null + if ( $Path -match '^\.{1,2}([\\\/]|$)') { + $Path = [System.IO.Path]::Combine($PodeContext.Server.Root, $Path.Substring(2)) + } + + if (!(Test-Path -Path $Path)) { + New-Item -ItemType Directory -Path $Path | Out-Null } # Capture process memory details @@ -3936,61 +3996,66 @@ function Invoke-PodeDumpInternal { ) # Capture the code causing the exception - $scriptContext = @( - [Ordered]@{ + $scriptContext = @() + $exceptionDetails = @() + $stackTrace = '' + + if ($null -ne $ErrorRecord) { + + $scriptContext += [Ordered]@{ ScriptName = $ErrorRecord.InvocationInfo.ScriptName Line = $ErrorRecord.InvocationInfo.Line PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage } - ) - # Capture stack trace information if available - $stackTrace = if ($ErrorRecord.Exception.StackTrace) { - $ErrorRecord.Exception.StackTrace -split "`n" - } - else { - 'No stack trace available' - } + # Capture stack trace information if available + $stackTrace = if ($ErrorRecord.Exception.StackTrace) { + $ErrorRecord.Exception.StackTrace -split "`n" + } + else { + 'No stack trace available' + } - # Capture exception details - $exceptionDetails = @( - [Ordered]@{ + # Capture exception details + $exceptionDetails += [Ordered]@{ ExceptionType = $ErrorRecord.Exception.GetType().FullName Message = $ErrorRecord.Exception.Message InnerException = if ($ErrorRecord.Exception.InnerException) { $ErrorRecord.Exception.InnerException.Message } else { $null } } - ) + } # Collect variables by scope - $scopedVariables = Get-ScopedVariable + $scopedVariables = Get-PodeDumpScopedVariable # Check if RunspacePools is not null before iterating $runspacePoolDetails = @() + + + <# Reflection in powershell + foreach ($r in $PodeContext.Runspaces) { + try { + # Define BindingFlags for non-public and instance members + $Flag = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance + + # Access _worker field + $_worker = $r.Pipeline.GetType().GetField('_worker', $Flag) + $worker = $_worker.GetValue($r.Pipeline) + + # Access CurrentlyRunningPipeline property + $_CRPProperty = $worker.GetType().GetProperty('CurrentlyRunningPipeline', $Flag) + $currentPipeline = $_CRPProperty.GetValue($worker) + + # Access the Runspace + $runspace = $currentPipeline.Runspace + + #> + + if ($null -ne $PodeContext.RunspacePools) { - write-podehost $PodeContext -explode -showtype - write-podehost 'sss' foreach ($poolName in $PodeContext.RunspacePools.Keys) { $pool = $PodeContext.RunspacePools[$poolName] if ($null -ne $pool -and $null -ne $pool.Pool) { - $runspaceVariables = @() - - if ($pool.Pool.RunspacePoolStateInfo.State -eq 'Opened') { - # write-podehost $pool.Pool -Explode -ShowType - foreach ($runspace in $pool.Pool.GetRunspaces()) { - $variables = $runspace.SessionStateProxy.InvokeCommand.InvokeScript({ - Get-ScopedVariable - }) - $runspaceVariables += @( - [Ordered]@{ - RunspaceId = $runspace.InstanceId - ThreadId = $runspace.GetExecutionContext().CurrentThread.ManagedThreadId - Variables = $variables - } - ) - } - } - $runspacePoolDetails += @( [Ordered]@{ PoolName = $poolName @@ -4018,33 +4083,31 @@ function Invoke-PodeDumpInternal { # Determine file extension and save format based on selected Format switch ($Format) { 'json' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" $dumpInfo | ConvertTo-Json -Depth $MaxDepth | Out-File -FilePath $dumpFilePath } 'clixml' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" $dumpInfo | Export-Clixml -Path $dumpFilePath } 'txt' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" $dumpInfo | Out-String | Out-File -FilePath $dumpFilePath } 'bin' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" [System.IO.File]::WriteAllBytes($dumpFilePath, [System.Text.Encoding]::UTF8.GetBytes([System.Management.Automation.PSSerializer]::Serialize($dumpInfo, 1))) } 'yaml' { - $dumpFilePath = Join-Path -Path $dumpDirectory -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" - $dumpInfo | ConvertTo-PodeYaml | Out-File -FilePath $dumpFilePath + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" + $dumpInfo | ConvertTo-PodeYaml -Depth $MaxDepth | Out-File -FilePath $dumpFilePath } } Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" - - # If Halt switch is specified, close the application - if ($Halt) { - Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' - Stop-Process -Id $PID -Force - } + } + end { + Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump + $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() } } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 1ffc30920..e28df41e1 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -146,7 +146,6 @@ function Start-PodeInternalServer { # run running event hooks Invoke-PodeEvent -Type Running - # state what endpoints are being listened on if ($endpoints.Length -gt 0) { @@ -167,46 +166,57 @@ function Start-PodeInternalServer { Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow } - # state the OpenAPI endpoints for each definition - foreach ($key in $PodeContext.Server.OpenAPI.Definitions.keys) { - $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks - if ( $bookmarks) { - Write-PodeHost - if (!$OpenAPIHeader) { - # OpenAPI Info - Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor Yellow - $OpenAPIHeader = $true + } + # state the OpenAPI endpoints for each definition + foreach ($key in $PodeContext.Server.OpenAPI.Definitions.keys) { + $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks + if ( $bookmarks) { + Write-PodeHost + if (!$OpenAPIHeader) { + # OpenAPI Info + Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor Green + $OpenAPIHeader = $true + } + Write-PodeHost " '$key':" -ForegroundColor Yellow + + if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor White } - Write-PodeHost " '$key':" -ForegroundColor Yellow - - if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { - # Specification - Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow - foreach ($endpoint in $bookmarks.route.Endpoint) { - Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor Yellow - } - # Documentation - Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow - foreach ($endpoint in $bookmarks.route.Endpoint) { - Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor Yellow - } + # Documentation + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor White } - else { - # Specification - Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow - $endpoints | ForEach-Object { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) - Write-PodeHost " . $url" -ForegroundColor Yellow - } - Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow - $endpoints | ForEach-Object { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) - Write-PodeHost " . $url" -ForegroundColor Yellow - } + } + else { + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow + $endpoints | ForEach-Object { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) + Write-PodeHost " . $url" -ForegroundColor White + } + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow + $endpoints | ForEach-Object { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) + Write-PodeHost " . $url" -ForegroundColor White } } } } + + if (! $PodeContext.Server.DisableTermination) { + Write-PodeHost + Write-PodeHost 'Server Control Commands:' -ForegroundColor Green + Write-PodeHost ' Ctrl+C : Gracefully terminate the server.' -ForegroundColor Cyan + Write-PodeHost ' Ctrl+R : Restart the server and reload configurations.' -ForegroundColor Cyan + if ($PodeContext.Server.Debug.Dump.Enabled) { + Write-PodeHost ' Ctrl+D : Generate a diagnostic dump for debugging purposes.' -ForegroundColor Cyan + } + } + } catch { throw diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 45e48e890..f6c4706be 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -208,14 +208,18 @@ function Start-PodeServer { # get the next key presses $key = Get-PodeConsoleKey - + # check for internal restart if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { Restart-PodeInternalServer } - if (($PodeContext.Tokens.Dump.IsCancellationRequested) ) { - Invoke-PodeDumpInternal -Halt + if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { + Invoke-PodeDumpInternal + if ($PodeContext.Server.Debug.Dump.Param.Halt) { + Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' + break + } } # check for open browser diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 568cca664..a758f7af6 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1489,19 +1489,83 @@ function Invoke-PodeGC { + <# .SYNOPSIS -Invokes the Dump. + Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. .DESCRIPTION -Invokes the Dump. + The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and + variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within + the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process + after saving the dump. + +.PARAMETER ErrorRecord + The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. + Accepts input from the pipeline. + +.PARAMETER Format + Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. + +.PARAMETER Halt + Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. + +.PARAMETER Path + Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. + +.PARAMETER MaxDepth + Specifies the maximum depth of objects to serialize when saving the dump in JSON or YAML + +.EXAMPLE + try { + # Simulate a critical error + throw [System.OutOfMemoryException] "Simulated out of memory error" + } + catch { + # Capture the dump in JSON format and halt the application + $_ | Invoke-PodeDump -Format 'json' -Halt + } + + This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. .EXAMPLE -Invoke-PodeDump + try { + # Simulate a critical error + throw [System.AccessViolationException] "Simulated access violation error" + } + catch { + # Capture the dump in YAML format without halting + $_ | Invoke-PodeDump -Format 'yaml' + } + + This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. + +.NOTES + This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. + It may be further adapted to log additional details or support different formats for captured data. + #> function Invoke-PodeDump { [CmdletBinding()] - param( ) + param( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + [Parameter()] + [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] + [string] + $Format, + + [string] + $Path, + + [switch] + $Halt, + + [int] + $MaxDepth + ) + write-podehost -explode $PSBoundParameters + $PodeContext.Server.Debug.Dump.Param = $PSBoundParameters $PodeContext.Tokens.Dump.Cancel() } \ No newline at end of file From 14afc55ed9baff6940c67bed48066809dfe7a42a Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 4 Nov 2024 10:35:36 -0800 Subject: [PATCH 05/34] Update Helpers.ps1 --- src/Private/Helpers.ps1 | 107 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 6 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 58c90bca6..b9d45f254 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3836,7 +3836,7 @@ function Get-PodeDumpScopedVariable { elseif ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { return $InputObject | ForEach-Object { ConvertTo-SerializableObject -InputObject $_ -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) } } - else { + else { return $InputObject.ToString() } } @@ -4031,8 +4031,8 @@ function Invoke-PodeDumpInternal { $runspacePoolDetails = @() - <# Reflection in powershell - foreach ($r in $PodeContext.Runspaces) { + # Reflection in powershell + <# foreach ($r in $PodeContext.Runspaces) { try { # Define BindingFlags for non-public and instance members $Flag = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance @@ -4048,8 +4048,15 @@ function Invoke-PodeDumpInternal { # Access the Runspace $runspace = $currentPipeline.Runspace - #> - + # Enable debugging for the runspace + #Enable-RunspaceDebug -BreakAll -Runspace $runspace + $vars = Get-PodeRunspaceVariablesViaDebugger -Runspace $runspace + start-sleep 3 + } + catch { + write-podehost $_`` + } + }#> if ($null -ne $PodeContext.RunspacePools) { foreach ($poolName in $PodeContext.RunspacePools.Keys) { @@ -4110,4 +4117,92 @@ function Invoke-PodeDumpInternal { Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() } -} \ No newline at end of file +} +function Get-PodeRunspaceVariablesViaDebugger { + param ( + [System.Management.Automation.Runspaces.Runspace]$Runspace + ) + + $variables = @() + + + + try { + # Attach the DebuggerStop event + + # Register-ObjectEvent -InputObject $Runspace.Debugger -EventName 'DebuggerStop' -Action { param ($sender, $args); Write-PodeHost 'Debugger stopped!'; $Runspace.Debugger.SetDebuggerStepMode($true) } + + + $Runspace.Debugger.SetDebuggerStepMode($true) + # Enable debugging and break all + Enable-RunspaceDebug -BreakAll -Runspace $Runspace + + + # Continuously check for the breakpoint state + while ($true) { + if ($Runspace.Debugger.InBreakpoint) { + # Prepare the command to run in the debugger + $command = [System.Management.Automation.PSCommand]::new().AddCommand('Get-Variable') + $outputCollection = [System.Management.Automation.PSDataCollection[System.Management.Automation.PSObject]]::new() + + # Process the command within the debugger safely + $Runspace.Debugger.ProcessCommand($command, $outputCollection) + + # Collect and parse the output + foreach ($output in $outputCollection) { + $variables += [PSCustomObject]@{ + Name = $output.Properties['Name'].Value + Value = $output.Properties['Value'].Value + } + } + + # Resume execution after capturing variables + $Runspace.Debugger.SetDebuggerAction([System.Management.Automation.DebuggerResumeAction]::Continue) + break + } + Start-Sleep -Milliseconds 100 + } + } + finally { + # Disable debugging for the runspace + Disable-RunspaceDebug -Runspace $Runspace + # Unregister-ObjectEvent -InputObject $Runspace.Debugger -EventName 'DebuggerStop' + } + + return $variables +} + + +function Invoke-PodeDebuggerStopEvent { + param ( + [System.Management.Automation.Runspaces.Runspace]$Runspace + ) + + try { + # Using reflection to get the protected RaiseDebuggerStopEvent method + $methodInfo = $Runspace.Debugger.GetType().GetMethod("RaiseDebuggerStopEvent", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) + + if ($null -eq $methodInfo) { + Write-Error "Could not find the method RaiseDebuggerStopEvent." + return + } + + # Create an empty collection of breakpoints + $breakpoints = [System.Collections.ObjectModel.Collection[System.Management.Automation.Breakpoint]]::new() + + # Set resume action to Continue + $resumeAction = [System.Management.Automation.DebuggerResumeAction]::Stop + + # Create the DebuggerStopEventArgs + $eventArgs = [System.Management.Automation.DebuggerStopEventArgs]::new($null, $breakpoints, $resumeAction) + + # Invoke the method + $methodInfo.Invoke($Runspace.Debugger, @($eventArgs)) + + Write-Host "DebuggerStopEvent raised successfully." + } + catch { + Write-Error "Error invoking RaiseDebuggerStopEvent: $_" + } +} + From 34d7b6b138de3acea2d470f58ccd22e74d8ba78c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 4 Nov 2024 17:11:24 -0800 Subject: [PATCH 06/34] added variables from runspace --- src/Private/Dump.ps1 | 515 ++++++++++++++++++++++++++++++++++++++++ src/Private/Helpers.ps1 | 429 +-------------------------------- 2 files changed, 516 insertions(+), 428 deletions(-) create mode 100644 src/Private/Dump.ps1 diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 new file mode 100644 index 000000000..649a64220 --- /dev/null +++ b/src/Private/Dump.ps1 @@ -0,0 +1,515 @@ + + +<# +.SYNOPSIS + Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. + +.DESCRIPTION + The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and + variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within + the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process + after saving the dump. + +.PARAMETER ErrorRecord + The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. + Accepts input from the pipeline. + +.PARAMETER Format + Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. + +.PARAMETER Halt + Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. + +.PARAMETER Path + Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. + +.EXAMPLE + try { + # Simulate a critical error + throw [System.OutOfMemoryException] "Simulated out of memory error" + } + catch { + # Capture the dump in JSON format and halt the application + $_ | Invoke-PodeDump -Format 'json' -Halt + } + + This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. + +.EXAMPLE + try { + # Simulate a critical error + throw [System.AccessViolationException] "Simulated access violation error" + } + catch { + # Capture the dump in YAML format without halting + $_ | Invoke-PodeDump -Format 'yaml' + } + + This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. + +.NOTES + This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. + It may be further adapted to log additional details or support different formats for captured data. + +#> +function Invoke-PodeDumpInternal { + param ( + [Parameter(Mandatory = $false, ValueFromPipeline = $true)] + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + + [Parameter()] + [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] + [string] + $Format, + + [string] + $Path, + + [int] + $MaxDepth + ) + + # Begin block to handle pipeline input + begin { + + # Default format and path from PodeContext + if ([string]::IsNullOrEmpty($Format)) { + if ($PodeContext.Server.Debug.Dump.Param.Format) { + $Format = $PodeContext.Server.Debug.Dump.Param.Format + } + else { + $Format = $PodeContext.Server.Debug.Dump.Format + } + } + if ([string]::IsNullOrEmpty($Path)) { + if ($PodeContext.Server.Debug.Dump.Param.Path) { + $Path = $PodeContext.Server.Debug.Dump.Param.Path + } + else { + $Path = $PodeContext.Server.Debug.Dump.Path + } + } + if ($null -eq $ErrorRecord) { + if ($PodeContext.Server.Debug.Dump.Param.ErrorRecord) { + $ErrorRecord = $PodeContext.Server.Debug.Dump.Param.ErrorRecord + } + else { + $ErrorRecord = $null + } + } + + if ($MaxDepth -lt 1) { + if ($PodeContext.Server.Debug.Dump.Param.MaxDepth) { + $MaxDepth = $PodeContext.Server.Debug.Dump.Param.MaxDepth + } + else { + $MaxDepth = $PodeContext.Server.Debug.Dump.MaxDepth + } + } + $PodeContext.Server.Debug.Dump.Param.Clear() + + Write-PodeHost -ForegroundColor Yellow 'Preparing Memory Dump ...' + } + + # Process block to handle each pipeline input + process { + # Ensure Dump directory exists in the specified path + if ( $Path -match '^\.{1,2}([\\\/]|$)') { + $Path = [System.IO.Path]::Combine($PodeContext.Server.Root, $Path.Substring(2)) + } + + if (!(Test-Path -Path $Path)) { + New-Item -ItemType Directory -Path $Path | Out-Null + } + + # Capture process memory details + $process = Get-Process -Id $PID + $memoryDetails = @( + [Ordered]@{ + ProcessId = $process.Id + ProcessName = $process.ProcessName + WorkingSet = [math]::Round($process.WorkingSet64 / 1MB, 2) + PrivateMemory = [math]::Round($process.PrivateMemorySize64 / 1MB, 2) + VirtualMemory = [math]::Round($process.VirtualMemorySize64 / 1MB, 2) + } + ) + + # Capture the code causing the exception + $scriptContext = @() + $exceptionDetails = @() + $stackTrace = '' + + if ($null -ne $ErrorRecord) { + + $scriptContext += [Ordered]@{ + ScriptName = $ErrorRecord.InvocationInfo.ScriptName + Line = $ErrorRecord.InvocationInfo.Line + PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage + } + + # Capture stack trace information if available + $stackTrace = if ($ErrorRecord.Exception.StackTrace) { + $ErrorRecord.Exception.StackTrace -split "`n" + } + else { + 'No stack trace available' + } + + # Capture exception details + $exceptionDetails += [Ordered]@{ + ExceptionType = $ErrorRecord.Exception.GetType().FullName + Message = $ErrorRecord.Exception.Message + InnerException = if ($ErrorRecord.Exception.InnerException) { $ErrorRecord.Exception.InnerException.Message } else { $null } + } + } + + # Collect variables by scope + $scopedVariables = Get-PodeDumpScopedVariable + + # Check if RunspacePools is not null before iterating + $runspacePoolDetails = @() + $runspaces = Get-Runspace + $runspaceDetails = @{} + foreach ($r in $runspaces) { + if ($r.Name.StartsWith('Pode_') ) { + $runspaceDetails[$r.Name] = @{ + Id = $r.Id + Name = $r.Name + InitialSessionState = $r.InitialSessionState + RunspaceStateInfo = $r.RunspaceStateInfo + } + $runspaceDetails[$r.Name].ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r + + } + } + + if ($null -ne $PodeContext.RunspacePools) { + foreach ($poolName in $PodeContext.RunspacePools.Keys) { + $pool = $PodeContext.RunspacePools[$poolName] + + if ($null -ne $pool -and $null -ne $pool.Pool) { + $runspacePoolDetails += @( + [Ordered]@{ + PoolName = $poolName + State = $pool.State + Result = $pool.result + InstanceId = $pool.Pool.InstanceId + IsDisposed = $pool.Pool.IsDisposed + RunspacePoolStateInfo = $pool.Pool.RunspacePoolStateInfo + InitialSessionState = $pool.Pool.InitialSessionState + CleanupInterval = $pool.Pool.CleanupInterval + RunspacePoolAvailability = $pool.Pool.RunspacePoolAvailability + ThreadOptions = $pool.Pool.ThreadOptions + } + ) + } + } + } + + # Combine all captured information into a single object + $dumpInfo = [Ordered]@{ + Timestamp = (Get-Date).ToString('s') + Memory = $memoryDetails + ScriptContext = $scriptContext + StackTrace = $stackTrace + ExceptionDetails = $exceptionDetails + ScopedVariables = $scopedVariables + RunspacePools = $runspacePoolDetails + Runspace = $runspaceDetails + } + + # Determine file extension and save format based on selected Format + switch ($Format) { + 'json' { + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" + $dumpInfo | ConvertTo-Json -Depth $MaxDepth -WarningAction SilentlyContinue | Out-File -FilePath $dumpFilePath + break + } + 'clixml' { + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" + $dumpInfo | Export-Clixml -Path $dumpFilePath + break + } + 'txt' { + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" + $dumpInfo | Out-String | Out-File -FilePath $dumpFilePath + break + } + 'bin' { + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" + [System.IO.File]::WriteAllBytes($dumpFilePath, [System.Text.Encoding]::UTF8.GetBytes([System.Management.Automation.PSSerializer]::Serialize($dumpInfo, $MaxDepth ))) + break + } + 'yaml' { + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" + $dumpInfo | ConvertTo-PodeYaml -Depth $MaxDepth | Out-File -FilePath $dumpFilePath + break + } + } + + Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" + } + end { + Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump + $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() + } +} +function Get-PodeRunspaceVariablesViaDebugger { + param ( + [Parameter(Mandatory)] + [System.Management.Automation.Runspaces.Runspace]$Runspace + ) + + $variables = @() + try { + Add-Type @' +using System; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Collections.ObjectModel; + +public class DebuggerHandler +{ + private static PSDataCollection variables = new PSDataCollection(); + private static EventHandler debuggerStopHandler; + private static bool eventTriggered = false; + + public static void AttachDebugger(Runspace runspace) + { + debuggerStopHandler = new EventHandler(OnDebuggerStop); + runspace.Debugger.DebuggerStop += debuggerStopHandler; + } + + public static void DetachDebugger(Runspace runspace) + { + if (debuggerStopHandler != null) + { + runspace.Debugger.DebuggerStop -= debuggerStopHandler; + debuggerStopHandler = null; + } + } + + private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) + { + eventTriggered = true; + + var debugger = sender as Debugger; + Console.WriteLine("Debugger stop event triggered."); + if (debugger != null) + { + debugger.SetDebuggerStepMode(true); + + // Prepare the command to run in the debugger + PSCommand command = new PSCommand(); + command.AddCommand("Get-PodeDumpScopedVariable"); + + // Create output collection for ProcessCommand + PSDataCollection outputCollection = new PSDataCollection(); + + // Process the command within the debugger + debugger.ProcessCommand(command, outputCollection); + + // Store output in a static collection + foreach (var output in outputCollection) + { + variables.Add(output); + } + + // Resume execution + // debugger.SetDebuggerAction(DebuggerResumeAction.Continue); + // Console.WriteLine("Debugger resumed."); + }else{ + Console.WriteLine("Debugger stop event triggered, but no debugger found."); + } + + } + + public static bool IsEventTriggered() + { + return eventTriggered; + } + + public static PSDataCollection GetVariables() + { + return variables; + } +} +'@ + # Attach the debugger using the embedded C# method + [DebuggerHandler]::AttachDebugger($Runspace) + # $Runspace.Debugger.SetDebuggerStepMode($true) + # Enable debugging and break all + Enable-RunspaceDebug -BreakAll -Runspace $Runspace + + Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine + + # Wait for the event to be triggered + while (! [DebuggerHandler]::IsEventTriggered()) { + Start-Sleep -Milliseconds 1000 + Write-PodeHost '.' -NoNewLine + } + + Write-PodeHost 'Done' + Start-Sleep -Milliseconds 1000 + # Retrieve and output the collected variables from the embedded C# code + $variables = [DebuggerHandler]::GetVariables() + + } + catch { + Write-Error -Message $_ + } + finally { + [DebuggerHandler]::DetachDebugger($Runspace) + # Disable debugging for the runspace + Disable-RunspaceDebug -Runspace $Runspace + } + + return $variables[0] +} + + +function Invoke-PodeDebuggerStopEvent { + param ( + [System.Management.Automation.Runspaces.Runspace]$Runspace + ) + + try { + # Using reflection to get the protected RaiseDebuggerStopEvent method + $methodInfo = $Runspace.Debugger.GetType().GetMethod('RaiseDebuggerStopEvent', [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) + + if ($null -eq $methodInfo) { + Write-Error 'Could not find the method RaiseDebuggerStopEvent.' + return + } + + # Create an empty collection of breakpoints + $breakpoints = [System.Collections.ObjectModel.Collection[System.Management.Automation.Breakpoint]]::new() + + # Set resume action to Continue + $resumeAction = [System.Management.Automation.DebuggerResumeAction]::Stop + + # Create the DebuggerStopEventArgs + $eventArgs = [System.Management.Automation.DebuggerStopEventArgs]::new($null, $breakpoints, $resumeAction) + + # Invoke the method + $methodInfo.Invoke($Runspace.Debugger, @($eventArgs)) + + Write-Host 'DebuggerStopEvent raised successfully.' + } + catch { + Write-Error "Error invoking RaiseDebuggerStopEvent: $_" + } +} + + + + +function Get-RunspaceFromPipeline { + param( + [System.Management.Automation.PowerShell] + $Pipeline + ) + + if ($null -ne $Pipeline.Runspace) { + return $Pipeline.Runspace + } + # Define BindingFlags for non-public and instance members + $Flag = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance + + # Access _worker field + $_worker = $Pipeline.GetType().GetField('_worker', $Flag) + $worker = $_worker.GetValue($Pipeline) + + # Access CurrentlyRunningPipeline property + $_CRPProperty = $worker.GetType().GetProperty('CurrentlyRunningPipeline', $Flag) + $currentPipeline = $_CRPProperty.GetValue($worker) + + # return Runspace + return $currentPipeline.Runspace +} + + + + +# Function to collect variables by scope +function Get-PodeDumpScopedVariable { + param ( + [int] + $MaxDepth = 5 # Default max depth + ) + # Safeguard against deeply nested objects + function ConvertTo-SerializableObject { + param ( + [object]$InputObject, + [int]$MaxDepth = 5, # Default max depth + [int]$CurrentDepth = 0 + ) + + if ($CurrentDepth -ge $MaxDepth) { + return 'Max depth reached' + } + + if ($null -eq $InputObject ) { + return $null + } + elseif ( $InputObject -is [hashtable]) { + $result = @{} + try { + foreach ($key in $InputObject.Keys) { + try { + $strKey = $key.ToString() + $result[$strKey] = ConvertTo-SerializableObject -InputObject $InputObject[$key] -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + catch { + write-podehost $_ -ForegroundColor Red + } + } + } + catch { + write-podehost $_ -ForegroundColor Red + } + return $result + } + elseif ($InputObject -is [PSCustomObject]) { + $result = @{} + try { + foreach ($property in $InputObject.PSObject.Properties) { + try { + $result[$property.Name.ToString()] = ConvertTo-SerializableObject -InputObject $property.Value -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + catch { + write-podehost $_ -ForegroundColor Red + } + } + } + catch { + write-podehost $_ -ForegroundColor Red + } + return $result + } + elseif ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { + return $InputObject | ForEach-Object { ConvertTo-SerializableObject -InputObject $_ -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) } + } + else { + return $InputObject.ToString() + } + } + + $scopes = @{ + Local = Get-Variable -Scope 0 + Script = Get-Variable -Scope Script + Global = Get-Variable -Scope Global + } + + $scopedVariables = @{} + foreach ($scope in $scopes.Keys) { + $variables = @{} + foreach ($var in $scopes[$scope]) { + $variables[$var.Name] = try { $var.Value } catch { 'Error retrieving value' } + } + $scopedVariables[$scope] = ConvertTo-SerializableObject -InputObject $variables -MaxDepth $MaxDepth + } + return $scopedVariables +} + diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index b9d45f254..8720d51a9 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3778,431 +3778,4 @@ function Copy-PodeObjectDeepClone { } - - -# Function to collect variables by scope -function Get-PodeDumpScopedVariable { - param () - # Safeguard against deeply nested objects - function ConvertTo-SerializableObject { - param ( - [object]$InputObject, - [int]$MaxDepth = 5, # Default max depth - [int]$CurrentDepth = 0 - ) - - if ($CurrentDepth -ge $MaxDepth) { - return 'Max depth reached' - } - - if ($null -eq $InputObject ) { - return $null - } - elseif ($InputObject -is [System.Collections.ListDictionaryInternal] -or $InputObject -is [hashtable]) { - $result = @{} - try { - foreach ($key in $InputObject.Keys) { - try { - $strKey = $key.ToString() - $result[$strKey] = ConvertTo-SerializableObject -InputObject $InputObject[$key] -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) - } - catch { - write-podehost $_ -ForegroundColor Red - } - } - } - catch { - write-podehost $_ -ForegroundColor Red - } - return $result - } - elseif ($InputObject -is [PSCustomObject]) { - $result = @{} - try { - foreach ($property in $InputObject.PSObject.Properties) { - try { - $result[$property.Name.ToString()] = ConvertTo-SerializableObject -InputObject $property.Value -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) - } - catch { - write-podehost $_ -ForegroundColor Red - } - } - } - catch { - write-podehost $_ -ForegroundColor Red - } - return $result - } - elseif ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { - return $InputObject | ForEach-Object { ConvertTo-SerializableObject -InputObject $_ -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) } - } - else { - return $InputObject.ToString() - } - } - - $scopes = @{ - Local = Get-Variable -Scope 0 - Script = Get-Variable -Scope Script - Global = Get-Variable -Scope Global - } - - $scopedVariables = @{} - foreach ($scope in $scopes.Keys) { - $variables = @{} - foreach ($var in $scopes[$scope]) { - $variables[$var.Name] = try { $var.Value } catch { 'Error retrieving value' } - } - $scopedVariables[$scope] = ConvertTo-SerializableObject -InputObject $variables -MaxDepth $MaxDepth - } - return $scopedVariables -} - - - -<# -.SYNOPSIS - Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. - -.DESCRIPTION - The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and - variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within - the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process - after saving the dump. - -.PARAMETER ErrorRecord - The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. - Accepts input from the pipeline. - -.PARAMETER Format - Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. - -.PARAMETER Halt - Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. - -.PARAMETER Path - Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. - -.EXAMPLE - try { - # Simulate a critical error - throw [System.OutOfMemoryException] "Simulated out of memory error" - } - catch { - # Capture the dump in JSON format and halt the application - $_ | Invoke-PodeDump -Format 'json' -Halt - } - - This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. - -.EXAMPLE - try { - # Simulate a critical error - throw [System.AccessViolationException] "Simulated access violation error" - } - catch { - # Capture the dump in YAML format without halting - $_ | Invoke-PodeDump -Format 'yaml' - } - - This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. - -.NOTES - This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. - It may be further adapted to log additional details or support different formats for captured data. - -#> -function Invoke-PodeDumpInternal { - param ( - [Parameter(Mandatory = $false, ValueFromPipeline = $true)] - [System.Management.Automation.ErrorRecord] - $ErrorRecord, - - [Parameter()] - [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] - [string] - $Format, - - [string] - $Path, - - [int] - $MaxDepth - ) - - # Begin block to handle pipeline input - begin { - - # Default format and path from PodeContext - if ([string]::IsNullOrEmpty($Format)) { - if ($PodeContext.Server.Debug.Dump.Param.Format) { - $Format = $PodeContext.Server.Debug.Dump.Param.Format - } - else { - $Format = $PodeContext.Server.Debug.Dump.Format - } - } - if ([string]::IsNullOrEmpty($Path)) { - if ($PodeContext.Server.Debug.Dump.Param.Path) { - $Path = $PodeContext.Server.Debug.Dump.Param.Path - } - else { - $Path = $PodeContext.Server.Debug.Dump.Path - } - } - if ($null -eq $ErrorRecord) { - if ($PodeContext.Server.Debug.Dump.Param.ErrorRecord) { - $ErrorRecord = $PodeContext.Server.Debug.Dump.Param.ErrorRecord - } - else { - $ErrorRecord = $null - } - } - - if ($MaxDepth -lt 1) { - if ($PodeContext.Server.Debug.Dump.Param.MaxDepth) { - $MaxDepth = $PodeContext.Server.Debug.Dump.Param.MaxDepth - } - else { - $MaxDepth = $PodeContext.Server.Debug.Dump.MaxDepth - } - } - $PodeContext.Server.Debug.Dump.Param.Clear() - - Write-PodeHost -ForegroundColor Yellow 'Preparing Memory Dump ...' - } - - # Process block to handle each pipeline input - process { - # Ensure Dump directory exists in the specified path - if ( $Path -match '^\.{1,2}([\\\/]|$)') { - $Path = [System.IO.Path]::Combine($PodeContext.Server.Root, $Path.Substring(2)) - } - - if (!(Test-Path -Path $Path)) { - New-Item -ItemType Directory -Path $Path | Out-Null - } - - # Capture process memory details - $process = Get-Process -Id $PID - $memoryDetails = @( - [Ordered]@{ - ProcessId = $process.Id - ProcessName = $process.ProcessName - WorkingSet = [math]::Round($process.WorkingSet64 / 1MB, 2) - PrivateMemory = [math]::Round($process.PrivateMemorySize64 / 1MB, 2) - VirtualMemory = [math]::Round($process.VirtualMemorySize64 / 1MB, 2) - } - ) - - # Capture the code causing the exception - $scriptContext = @() - $exceptionDetails = @() - $stackTrace = '' - - if ($null -ne $ErrorRecord) { - - $scriptContext += [Ordered]@{ - ScriptName = $ErrorRecord.InvocationInfo.ScriptName - Line = $ErrorRecord.InvocationInfo.Line - PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage - } - - # Capture stack trace information if available - $stackTrace = if ($ErrorRecord.Exception.StackTrace) { - $ErrorRecord.Exception.StackTrace -split "`n" - } - else { - 'No stack trace available' - } - - # Capture exception details - $exceptionDetails += [Ordered]@{ - ExceptionType = $ErrorRecord.Exception.GetType().FullName - Message = $ErrorRecord.Exception.Message - InnerException = if ($ErrorRecord.Exception.InnerException) { $ErrorRecord.Exception.InnerException.Message } else { $null } - } - } - - # Collect variables by scope - $scopedVariables = Get-PodeDumpScopedVariable - - # Check if RunspacePools is not null before iterating - $runspacePoolDetails = @() - - - # Reflection in powershell - <# foreach ($r in $PodeContext.Runspaces) { - try { - # Define BindingFlags for non-public and instance members - $Flag = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance - - # Access _worker field - $_worker = $r.Pipeline.GetType().GetField('_worker', $Flag) - $worker = $_worker.GetValue($r.Pipeline) - - # Access CurrentlyRunningPipeline property - $_CRPProperty = $worker.GetType().GetProperty('CurrentlyRunningPipeline', $Flag) - $currentPipeline = $_CRPProperty.GetValue($worker) - - # Access the Runspace - $runspace = $currentPipeline.Runspace - - # Enable debugging for the runspace - #Enable-RunspaceDebug -BreakAll -Runspace $runspace - $vars = Get-PodeRunspaceVariablesViaDebugger -Runspace $runspace - start-sleep 3 - } - catch { - write-podehost $_`` - } - }#> - - if ($null -ne $PodeContext.RunspacePools) { - foreach ($poolName in $PodeContext.RunspacePools.Keys) { - $pool = $PodeContext.RunspacePools[$poolName] - - if ($null -ne $pool -and $null -ne $pool.Pool) { - $runspacePoolDetails += @( - [Ordered]@{ - PoolName = $poolName - State = $pool.State - MaxThreads = $pool.Pool.MaxRunspaces - AvailableThreads = $pool.Pool.GetAvailableRunspaces() - RunspaceVariables = $runspaceVariables - } - ) - } - } - } - - # Combine all captured information into a single object - $dumpInfo = [Ordered]@{ - Timestamp = (Get-Date).ToString('s') - Memory = $memoryDetails - ScriptContext = $scriptContext - StackTrace = $stackTrace - ExceptionDetails = $exceptionDetails - ScopedVariables = $scopedVariables - RunspacePools = $runspacePoolDetails - } - - # Determine file extension and save format based on selected Format - switch ($Format) { - 'json' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" - $dumpInfo | ConvertTo-Json -Depth $MaxDepth | Out-File -FilePath $dumpFilePath - } - 'clixml' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" - $dumpInfo | Export-Clixml -Path $dumpFilePath - } - 'txt' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" - $dumpInfo | Out-String | Out-File -FilePath $dumpFilePath - } - 'bin' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" - [System.IO.File]::WriteAllBytes($dumpFilePath, [System.Text.Encoding]::UTF8.GetBytes([System.Management.Automation.PSSerializer]::Serialize($dumpInfo, 1))) - } - 'yaml' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" - $dumpInfo | ConvertTo-PodeYaml -Depth $MaxDepth | Out-File -FilePath $dumpFilePath - } - } - - Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" - } - end { - Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump - $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() - } -} -function Get-PodeRunspaceVariablesViaDebugger { - param ( - [System.Management.Automation.Runspaces.Runspace]$Runspace - ) - - $variables = @() - - - - try { - # Attach the DebuggerStop event - - # Register-ObjectEvent -InputObject $Runspace.Debugger -EventName 'DebuggerStop' -Action { param ($sender, $args); Write-PodeHost 'Debugger stopped!'; $Runspace.Debugger.SetDebuggerStepMode($true) } - - - $Runspace.Debugger.SetDebuggerStepMode($true) - # Enable debugging and break all - Enable-RunspaceDebug -BreakAll -Runspace $Runspace - - - # Continuously check for the breakpoint state - while ($true) { - if ($Runspace.Debugger.InBreakpoint) { - # Prepare the command to run in the debugger - $command = [System.Management.Automation.PSCommand]::new().AddCommand('Get-Variable') - $outputCollection = [System.Management.Automation.PSDataCollection[System.Management.Automation.PSObject]]::new() - - # Process the command within the debugger safely - $Runspace.Debugger.ProcessCommand($command, $outputCollection) - - # Collect and parse the output - foreach ($output in $outputCollection) { - $variables += [PSCustomObject]@{ - Name = $output.Properties['Name'].Value - Value = $output.Properties['Value'].Value - } - } - - # Resume execution after capturing variables - $Runspace.Debugger.SetDebuggerAction([System.Management.Automation.DebuggerResumeAction]::Continue) - break - } - Start-Sleep -Milliseconds 100 - } - } - finally { - # Disable debugging for the runspace - Disable-RunspaceDebug -Runspace $Runspace - # Unregister-ObjectEvent -InputObject $Runspace.Debugger -EventName 'DebuggerStop' - } - - return $variables -} - - -function Invoke-PodeDebuggerStopEvent { - param ( - [System.Management.Automation.Runspaces.Runspace]$Runspace - ) - - try { - # Using reflection to get the protected RaiseDebuggerStopEvent method - $methodInfo = $Runspace.Debugger.GetType().GetMethod("RaiseDebuggerStopEvent", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) - - if ($null -eq $methodInfo) { - Write-Error "Could not find the method RaiseDebuggerStopEvent." - return - } - - # Create an empty collection of breakpoints - $breakpoints = [System.Collections.ObjectModel.Collection[System.Management.Automation.Breakpoint]]::new() - - # Set resume action to Continue - $resumeAction = [System.Management.Automation.DebuggerResumeAction]::Stop - - # Create the DebuggerStopEventArgs - $eventArgs = [System.Management.Automation.DebuggerStopEventArgs]::new($null, $breakpoints, $resumeAction) - - # Invoke the method - $methodInfo.Invoke($Runspace.Debugger, @($eventArgs)) - - Write-Host "DebuggerStopEvent raised successfully." - } - catch { - Write-Error "Error invoking RaiseDebuggerStopEvent: $_" - } -} - + \ No newline at end of file From f05adda97473f5bc6b9b0b280013cb146855bb4d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 4 Nov 2024 17:25:02 -0800 Subject: [PATCH 07/34] Update Dump.ps1 --- src/Private/Dump.ps1 | 319 ++++++++++++++++++++++++++----------------- 1 file changed, 190 insertions(+), 129 deletions(-) diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 649a64220..647b49854 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -1,5 +1,3 @@ - - <# .SYNOPSIS Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. @@ -255,14 +253,51 @@ function Invoke-PodeDumpInternal { $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() } } + +<# +.SYNOPSIS + Collects scoped variables from a specified runspace using the PowerShell debugger. + +.DESCRIPTION + This function attaches a debugger to a given runspace, breaks execution, and collects scoped variables using a custom C# class. + It waits until the debugger stop event is triggered or until a specified timeout period elapses. + If the timeout is reached without triggering the event, it returns an empty hashtable. + +.PARAMETER Runspace + The runspace from which to collect scoped variables. This parameter is mandatory. + +.PARAMETER Timeout + The maximum time (in seconds) to wait for the debugger stop event to be triggered. Defaults to 60 seconds. + +.EXAMPLE + $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() + $runspace.Open() + $variables = Get-PodeRunspaceVariablesViaDebugger -Runspace $runspace -Timeout 30 + $runspace.Close() + + This example opens a runspace, collects scoped variables with a 30-second timeout, and then closes the runspace. + +.NOTES + The function uses an embedded C# class to handle the `DebuggerStop` event. This class attaches and detaches the debugger and processes commands in the stopped state. + The collected variables are returned as a `PSObject`. + +.COMPONENT + Pode + +#> function Get-PodeRunspaceVariablesViaDebugger { param ( [Parameter(Mandatory)] - [System.Management.Automation.Runspaces.Runspace]$Runspace + [System.Management.Automation.Runspaces.Runspace]$Runspace, + + [Parameter()] + [int]$Timeout = 60 ) + # Initialize variables collection $variables = @() try { + # Embed C# code to handle the DebuggerStop event Add-Type @' using System; using System.Management.Automation; @@ -271,245 +306,271 @@ using System.Collections.ObjectModel; public class DebuggerHandler { + // Collection to store variables collected during the debugging session private static PSDataCollection variables = new PSDataCollection(); + + // Event handler for the DebuggerStop event private static EventHandler debuggerStopHandler; + + // Flag to indicate whether the DebuggerStop event has been triggered private static bool eventTriggered = false; + // Method to attach the DebuggerStop event handler to the runspace's debugger public static void AttachDebugger(Runspace runspace) { + // Initialize the event handler with the OnDebuggerStop method debuggerStopHandler = new EventHandler(OnDebuggerStop); + + // Attach the event handler to the DebuggerStop event of the runspace's debugger runspace.Debugger.DebuggerStop += debuggerStopHandler; } + // Method to detach the DebuggerStop event handler from the runspace's debugger public static void DetachDebugger(Runspace runspace) { if (debuggerStopHandler != null) { + // Remove the event handler to prevent further event handling runspace.Debugger.DebuggerStop -= debuggerStopHandler; + + // Set the handler to null to clean up debuggerStopHandler = null; } } + // Event handler method that gets called when the debugger stops private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) { + // Set the eventTriggered flag to true eventTriggered = true; + // Cast the sender to a Debugger object var debugger = sender as Debugger; - Console.WriteLine("Debugger stop event triggered."); if (debugger != null) { + // Enable step mode to allow for command execution during the debug stop debugger.SetDebuggerStepMode(true); - // Prepare the command to run in the debugger + // Create a PSCommand to run the Get-PodeDumpScopedVariable command PSCommand command = new PSCommand(); command.AddCommand("Get-PodeDumpScopedVariable"); - // Create output collection for ProcessCommand + // Create a collection to store the command output PSDataCollection outputCollection = new PSDataCollection(); - // Process the command within the debugger + // Execute the command within the debugger debugger.ProcessCommand(command, outputCollection); - // Store output in a static collection + // Add each result to the variables collection foreach (var output in outputCollection) { variables.Add(output); } - - // Resume execution - // debugger.SetDebuggerAction(DebuggerResumeAction.Continue); - // Console.WriteLine("Debugger resumed."); - }else{ - Console.WriteLine("Debugger stop event triggered, but no debugger found."); } - } + // Method to check if the DebuggerStop event has been triggered public static bool IsEventTriggered() { return eventTriggered; } + // Method to retrieve the collected variables public static PSDataCollection GetVariables() { return variables; } } '@ + # Attach the debugger using the embedded C# method [DebuggerHandler]::AttachDebugger($Runspace) - # $Runspace.Debugger.SetDebuggerStepMode($true) + # Enable debugging and break all Enable-RunspaceDebug -BreakAll -Runspace $Runspace Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine - # Wait for the event to be triggered + # Initialize the timer + $startTime = [DateTime]::UtcNow + + # Wait for the event to be triggered or timeout while (! [DebuggerHandler]::IsEventTriggered()) { Start-Sleep -Milliseconds 1000 Write-PodeHost '.' -NoNewLine + + if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { + Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" + return @{} + } } Write-PodeHost 'Done' - Start-Sleep -Milliseconds 1000 + # Retrieve and output the collected variables from the embedded C# code $variables = [DebuggerHandler]::GetVariables() - } catch { - Write-Error -Message $_ + # Log the error details using Write-PodeErrorLog. + # This ensures that any exceptions thrown during the execution are logged appropriately. + $_ | Write-PodeErrorLog } finally { + # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. [DebuggerHandler]::DetachDebugger($Runspace) - # Disable debugging for the runspace + + # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. Disable-RunspaceDebug -Runspace $Runspace } return $variables[0] } +<# +.SYNOPSIS + Collects and serializes variables from different scopes (Local, Script, Global). -function Invoke-PodeDebuggerStopEvent { - param ( - [System.Management.Automation.Runspaces.Runspace]$Runspace - ) +.DESCRIPTION + This function retrieves variables from Local, Script, and Global scopes and serializes them to ensure they can be output or logged. + It includes a safeguard against deeply nested objects by limiting the depth of serialization to prevent stack overflow or excessive memory usage. - try { - # Using reflection to get the protected RaiseDebuggerStopEvent method - $methodInfo = $Runspace.Debugger.GetType().GetMethod('RaiseDebuggerStopEvent', [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) +.PARAMETER MaxDepth + Specifies the maximum depth for serializing nested objects. Defaults to 5 levels deep. - if ($null -eq $methodInfo) { - Write-Error 'Could not find the method RaiseDebuggerStopEvent.' - return - } +.EXAMPLE + Get-PodeDumpScopedVariable -MaxDepth 3 + + This example retrieves variables from all scopes and serializes them with a maximum depth of 3. - # Create an empty collection of breakpoints - $breakpoints = [System.Collections.ObjectModel.Collection[System.Management.Automation.Breakpoint]]::new() +.NOTES + This function is useful for debugging and logging purposes where variable data from different scopes needs to be safely serialized and inspected. - # Set resume action to Continue - $resumeAction = [System.Management.Automation.DebuggerResumeAction]::Stop +#> +function Get-PodeDumpScopedVariable { + param ( + [int] + $MaxDepth = 5 + ) - # Create the DebuggerStopEventArgs - $eventArgs = [System.Management.Automation.DebuggerStopEventArgs]::new($null, $breakpoints, $resumeAction) - # Invoke the method - $methodInfo.Invoke($Runspace.Debugger, @($eventArgs)) - Write-Host 'DebuggerStopEvent raised successfully.' + # Collect variables from Local, Script, and Global scopes + $scopes = @{ + Local = Get-Variable -Scope 0 + Script = Get-Variable -Scope Script + Global = Get-Variable -Scope Global } - catch { - Write-Error "Error invoking RaiseDebuggerStopEvent: $_" + + # Dictionary to hold serialized variables by scope + $scopedVariables = @{} + foreach ($scope in $scopes.Keys) { + $variables = @{} + foreach ($var in $scopes[$scope]) { + # Attempt to retrieve the variable's value, handling any errors + $variables[$var.Name] = try { $var.Value } catch { 'Error retrieving value' } + } + # Serialize the variables to ensure safe output + $scopedVariables[$scope] = ConvertTo-PodeSerializableObject -InputObject $variables -MaxDepth $MaxDepth } -} + # Return the serialized variables by scope + return $scopedVariables +} +<# +.SYNOPSIS + Safely serializes an object, ensuring it doesn't exceed the specified depth. +.DESCRIPTION + This function recursively serializes an object to a simpler, more displayable form, handling complex or deeply nested objects by limiting the serialization depth. + It supports various object types like hashtables, PSCustomObjects, and collections while avoiding overly deep recursion that could cause stack overflow or excessive resource usage. -function Get-RunspaceFromPipeline { - param( - [System.Management.Automation.PowerShell] - $Pipeline - ) +.PARAMETER InputObject + The object to be serialized. - if ($null -ne $Pipeline.Runspace) { - return $Pipeline.Runspace - } - # Define BindingFlags for non-public and instance members - $Flag = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance +.PARAMETER MaxDepth + Specifies the maximum depth for serialization. Defaults to 5 levels deep. - # Access _worker field - $_worker = $Pipeline.GetType().GetField('_worker', $Flag) - $worker = $_worker.GetValue($Pipeline) +.PARAMETER CurrentDepth + The current depth in the recursive serialization process. Defaults to 0 and is used internally during recursion. - # Access CurrentlyRunningPipeline property - $_CRPProperty = $worker.GetType().GetProperty('CurrentlyRunningPipeline', $Flag) - $currentPipeline = $_CRPProperty.GetValue($worker) +.EXAMPLE + ConvertTo-PodeSerializableObject -InputObject $complexObject -MaxDepth 3 - # return Runspace - return $currentPipeline.Runspace -} + This example serializes a complex object with a maximum depth of 3. +.NOTES + This function is useful for logging, debugging, and safely displaying complex objects in a readable format. +#> +function ConvertTo-PodeSerializableObject { + param ( + [object] + $InputObject, + [int] + $MaxDepth = 5, -# Function to collect variables by scope -function Get-PodeDumpScopedVariable { - param ( [int] - $MaxDepth = 5 # Default max depth + $CurrentDepth = 0 ) - # Safeguard against deeply nested objects - function ConvertTo-SerializableObject { - param ( - [object]$InputObject, - [int]$MaxDepth = 5, # Default max depth - [int]$CurrentDepth = 0 - ) - if ($CurrentDepth -ge $MaxDepth) { - return 'Max depth reached' - } + # Check if the current depth has reached or exceeded the maximum allowed depth + if ($CurrentDepth -ge $MaxDepth) { + # Return a simple message indicating that the maximum depth has been reached + return 'Max depth reached' + } - if ($null -eq $InputObject ) { - return $null - } - elseif ( $InputObject -is [hashtable]) { - $result = @{} - try { - foreach ($key in $InputObject.Keys) { - try { - $strKey = $key.ToString() - $result[$strKey] = ConvertTo-SerializableObject -InputObject $InputObject[$key] -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) - } - catch { - write-podehost $_ -ForegroundColor Red - } + # Handle null input + if ($null -eq $InputObject) { + return $null # Return null if the input object is null + } + # Handle hashtables + elseif ($InputObject -is [hashtable]) { + $result = @{} + try { + foreach ($key in $InputObject.Keys) { + try { + # Serialize each key-value pair in the hashtable + $strKey = $key.ToString() + $result[$strKey] = ConvertTo-PodeSerializableObject -InputObject $InputObject[$key] -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + catch { + Write-PodeHost $_ -ForegroundColor Red } } - catch { - write-podehost $_ -ForegroundColor Red - } - return $result } - elseif ($InputObject -is [PSCustomObject]) { - $result = @{} - try { - foreach ($property in $InputObject.PSObject.Properties) { - try { - $result[$property.Name.ToString()] = ConvertTo-SerializableObject -InputObject $property.Value -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) - } - catch { - write-podehost $_ -ForegroundColor Red - } + catch { + Write-PodeHost $_ -ForegroundColor Red + } + return $result + } + # Handle PSCustomObjects + elseif ($InputObject -is [PSCustomObject]) { + $result = @{} + try { + foreach ($property in $InputObject.PSObject.Properties) { + try { + # Serialize each property in the PSCustomObject + $result[$property.Name.ToString()] = ConvertTo-PodeSerializableObject -InputObject $property.Value -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) + } + catch { + Write-PodeHost $_ -ForegroundColor Red } } - catch { - write-podehost $_ -ForegroundColor Red - } - return $result } - elseif ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { - return $InputObject | ForEach-Object { ConvertTo-SerializableObject -InputObject $_ -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) } - } - else { - return $InputObject.ToString() + catch { + Write-PodeHost $_ -ForegroundColor Red } + return $result } - - $scopes = @{ - Local = Get-Variable -Scope 0 - Script = Get-Variable -Scope Script - Global = Get-Variable -Scope Global + # Handle enumerable collections, excluding strings + elseif ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { + # Serialize each item in the collection + return $InputObject | ForEach-Object { ConvertTo-PodeSerializableObject -InputObject $_ -MaxDepth $MaxDepth -CurrentDepth ($CurrentDepth + 1) } } - - $scopedVariables = @{} - foreach ($scope in $scopes.Keys) { - $variables = @{} - foreach ($var in $scopes[$scope]) { - $variables[$var.Name] = try { $var.Value } catch { 'Error retrieving value' } - } - $scopedVariables[$scope] = ConvertTo-SerializableObject -InputObject $variables -MaxDepth $MaxDepth + else { + # Convert other object types to string for serialization + return $InputObject.ToString() } - return $scopedVariables } - From b64da21039ea0b134bbf600163d1be1a53a1cd31 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 5 Nov 2024 10:23:14 -0800 Subject: [PATCH 08/34] Added suspend and resume server --- src/Private/Context.ps1 | 15 +-- src/Private/Dump.ps1 | 217 +++++++++++++++++++++----------------- src/Private/Endpoints.ps1 | 45 ++++++++ src/Private/Events.ps1 | 2 +- src/Private/Helpers.ps1 | 20 +++- src/Private/OpenApi.ps1 | 64 +++++++++++ src/Private/Server.ps1 | 209 +++++++++++++++++++++++------------- src/Public/Core.ps1 | 15 ++- src/Public/Runspaces.ps1 | 7 +- 9 files changed, 414 insertions(+), 180 deletions(-) diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index aa5ab6d28..36ce9d7e7 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -415,9 +415,10 @@ function New-PodeContext { # create new cancellation tokens $ctx.Tokens = @{ - Cancellation = [System.Threading.CancellationTokenSource]::new() - Restart = [System.Threading.CancellationTokenSource]::new() - Dump = [System.Threading.CancellationTokenSource]::new() + Cancellation = [System.Threading.CancellationTokenSource]::new() + Restart = [System.Threading.CancellationTokenSource]::new() + Dump = [System.Threading.CancellationTokenSource]::new() + SuspendResume = [System.Threading.CancellationTokenSource]::new() } # requests that should be logged @@ -913,11 +914,11 @@ function Set-PodeServerConfiguration { Enabled = [bool](Protect-PodeValue -Value $Configuration.Debug.Breakpoints.Enable -Default $Context.Server.Debug.Breakpoints.Enable) } Dump = @{ - Enabled = [bool](Protect-PodeValue -Value $Configuration.Debug.Dump.Enabled -Default $Context.Server.Debug.Dump.Enabled) - Format = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Format -Default $Context.Server.Debug.Dump.Format) - Path = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Path -Default $Context.Server.Debug.Dump.Path) + Enabled = [bool](Protect-PodeValue -Value $Configuration.Debug.Dump.Enabled -Default $Context.Server.Debug.Dump.Enabled) + Format = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Format -Default $Context.Server.Debug.Dump.Format) + Path = [string] (Protect-PodeValue -Value $Configuration.Debug.Dump.Path -Default $Context.Server.Debug.Dump.Path) MaxDepth = [int] (Protect-PodeValue -Value $Configuration.Debug.Dump.MaxDepth -Default $Context.Server.Debug.Dump.MaxDepth) - Param = @{} + Param = @{} } } } diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 647b49854..8edb66461 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -70,7 +70,7 @@ function Invoke-PodeDumpInternal { # Begin block to handle pipeline input begin { - + Invoke-PodeEvent -Type Dump # Default format and path from PodeContext if ([string]::IsNullOrEmpty($Format)) { if ($PodeContext.Server.Debug.Dump.Param.Format) { @@ -167,10 +167,9 @@ function Invoke-PodeDumpInternal { # Check if RunspacePools is not null before iterating $runspacePoolDetails = @() - $runspaces = Get-Runspace + $runspaces = Get-Runspace -name 'Pode_*' $runspaceDetails = @{} foreach ($r in $runspaces) { - if ($r.Name.StartsWith('Pode_') ) { $runspaceDetails[$r.Name] = @{ Id = $r.Id Name = $r.Name @@ -178,8 +177,6 @@ function Invoke-PodeDumpInternal { RunspaceStateInfo = $r.RunspaceStateInfo } $runspaceDetails[$r.Name].ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r - - } } if ($null -ne $PodeContext.RunspacePools) { @@ -297,94 +294,9 @@ function Get-PodeRunspaceVariablesViaDebugger { # Initialize variables collection $variables = @() try { - # Embed C# code to handle the DebuggerStop event - Add-Type @' -using System; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Collections.ObjectModel; - -public class DebuggerHandler -{ - // Collection to store variables collected during the debugging session - private static PSDataCollection variables = new PSDataCollection(); - - // Event handler for the DebuggerStop event - private static EventHandler debuggerStopHandler; - - // Flag to indicate whether the DebuggerStop event has been triggered - private static bool eventTriggered = false; - - // Method to attach the DebuggerStop event handler to the runspace's debugger - public static void AttachDebugger(Runspace runspace) - { - // Initialize the event handler with the OnDebuggerStop method - debuggerStopHandler = new EventHandler(OnDebuggerStop); - - // Attach the event handler to the DebuggerStop event of the runspace's debugger - runspace.Debugger.DebuggerStop += debuggerStopHandler; - } - - // Method to detach the DebuggerStop event handler from the runspace's debugger - public static void DetachDebugger(Runspace runspace) - { - if (debuggerStopHandler != null) - { - // Remove the event handler to prevent further event handling - runspace.Debugger.DebuggerStop -= debuggerStopHandler; - - // Set the handler to null to clean up - debuggerStopHandler = null; - } - } - - // Event handler method that gets called when the debugger stops - private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) - { - // Set the eventTriggered flag to true - eventTriggered = true; - - // Cast the sender to a Debugger object - var debugger = sender as Debugger; - if (debugger != null) - { - // Enable step mode to allow for command execution during the debug stop - debugger.SetDebuggerStepMode(true); - - // Create a PSCommand to run the Get-PodeDumpScopedVariable command - PSCommand command = new PSCommand(); - command.AddCommand("Get-PodeDumpScopedVariable"); - - // Create a collection to store the command output - PSDataCollection outputCollection = new PSDataCollection(); - - // Execute the command within the debugger - debugger.ProcessCommand(command, outputCollection); - - // Add each result to the variables collection - foreach (var output in outputCollection) - { - variables.Add(output); - } - } - } - - // Method to check if the DebuggerStop event has been triggered - public static bool IsEventTriggered() - { - return eventTriggered; - } - - // Method to retrieve the collected variables - public static PSDataCollection GetVariables() - { - return variables; - } -} -'@ # Attach the debugger using the embedded C# method - [DebuggerHandler]::AttachDebugger($Runspace) + [Pode.Embedded.DebuggerHandler]::AttachDebugger($Runspace, $true) # Enable debugging and break all Enable-RunspaceDebug -BreakAll -Runspace $Runspace @@ -395,7 +307,7 @@ public class DebuggerHandler $startTime = [DateTime]::UtcNow # Wait for the event to be triggered or timeout - while (! [DebuggerHandler]::IsEventTriggered()) { + while (! [Pode.Embedded.DebuggerHandler]::IsEventTriggered()) { Start-Sleep -Milliseconds 1000 Write-PodeHost '.' -NoNewLine @@ -408,7 +320,7 @@ public class DebuggerHandler Write-PodeHost 'Done' # Retrieve and output the collected variables from the embedded C# code - $variables = [DebuggerHandler]::GetVariables() + $variables = [Pode.Embedded.DebuggerHandler]::GetVariables() } catch { # Log the error details using Write-PodeErrorLog. @@ -417,7 +329,7 @@ public class DebuggerHandler } finally { # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. - [DebuggerHandler]::DetachDebugger($Runspace) + [Pode.Embedded.DebuggerHandler]::DetachDebugger($Runspace) # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. Disable-RunspaceDebug -Runspace $Runspace @@ -574,3 +486,120 @@ function ConvertTo-PodeSerializableObject { return $InputObject.ToString() } } + +function Initialize-DebugHandler { + + + # Embed C# code to handle the DebuggerStop event + Add-Type @' +using System; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Collections.ObjectModel; + +namespace Pode.Embedded +{ + public class DebuggerHandler + { + // Collection to store variables collected during the debugging session + private static PSDataCollection variables = new PSDataCollection(); + + // Event handler for the DebuggerStop event + private static EventHandler debuggerStopHandler; + + // Flag to indicate whether the DebuggerStop event has been triggered + private static bool eventTriggered = false; + + // Flag to control whether variables should be collected during the DebuggerStop event + private static bool shouldCollectVariables = true; + + // Method to attach the DebuggerStop event handler to the runspace's debugger + public static void AttachDebugger(Runspace runspace, bool collectVariables = true) + { + // Set the collection flag based on the parameter + shouldCollectVariables = collectVariables; + + // Initialize the event handler with the OnDebuggerStop method + debuggerStopHandler = new EventHandler(OnDebuggerStop); + + // Attach the event handler to the DebuggerStop event of the runspace's debugger + runspace.Debugger.DebuggerStop += debuggerStopHandler; + } + + // Method to detach the DebuggerStop event handler from the runspace's debugger + public static void DetachDebugger(Runspace runspace) + { + if (debuggerStopHandler != null) + { + // Remove the event handler to prevent further event handling + runspace.Debugger.DebuggerStop -= debuggerStopHandler; + + // Set the handler to null to clean up + debuggerStopHandler = null; + } + } + + // Event handler method that gets called when the debugger stops + private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) + { + // Set the eventTriggered flag to true + eventTriggered = true; + + // Cast the sender to a Debugger object + var debugger = sender as Debugger; + if (debugger != null) + { + // Enable step mode to allow for command execution during the debug stop + debugger.SetDebuggerStepMode(true); + + PSCommand command = new PSCommand(); + + if (shouldCollectVariables) + { + // Collect variables + command.AddCommand("Get-PodeDumpScopedVariable"); + } + else + { + // Execute a break + command.AddCommand( "while( $PodeContext.Server.Suspended){ Start-sleep 1}"); + } + + // Create a collection to store the command output + PSDataCollection outputCollection = new PSDataCollection(); + + // Execute the command within the debugger + debugger.ProcessCommand(command, outputCollection); + + // Add results to the variables collection if collecting variables + if (shouldCollectVariables) + { + foreach (var output in outputCollection) + { + variables.Add(output); + } + } + else + { + // Ensure the debugger remains ready for further interaction + debugger.SetDebuggerStepMode(true); + } + } + } + + + // Method to check if the DebuggerStop event has been triggered + public static bool IsEventTriggered() + { + return eventTriggered; + } + + // Method to retrieve the collected variables + public static PSDataCollection GetVariables() + { + return variables; + } + } +} +'@ +} \ No newline at end of file diff --git a/src/Private/Endpoints.ps1 b/src/Private/Endpoints.ps1 index 7cc6cf657..2ceda786b 100644 --- a/src/Private/Endpoints.ps1 +++ b/src/Private/Endpoints.ps1 @@ -385,4 +385,49 @@ function Get-PodeEndpointByName { } return $null +} + + +<# +.SYNOPSIS + Displays information about the endpoints the Pode server is listening on. + +.DESCRIPTION + The `Show-PodeEndPointConsoleInfo` function checks the Pode server's `EndpointsInfo` + and displays details about each endpoint, including its URL and any specific flags + such as `DualMode`. It provides a summary of the total number of endpoints and the + number of general threads handling them. + +.EXAMPLE + Show-PodeEndPointConsoleInfo + + This command will output details of all endpoints the Pode server is currently + listening on, including their URLs and any relevant flags. + +.NOTES + This function uses `Write-PodeHost` to display messages, with the `Yellow` foreground + color for clarity. It ensures each endpoint is displayed with its associated flags, + enhancing visibility of specific configurations like `DualMode`. +#> +function Show-PodeEndPointConsoleInfo { + if ($PodeContext.Server.EndpointsInfo.Length -gt 0) { + + # Listening on the following $endpoints.Length endpoint(s) [$PodeContext.Threads.General thread(s)] + Write-PodeHost ($PodeLocale.listeningOnEndpointsMessage -f $PodeContext.Server.EndpointsInfo.Length, $PodeContext.Threads.General) -ForegroundColor Yellow + $PodeContext.Server.EndpointsInfo | ForEach-Object { + $flags = @() + if ($_.DualMode) { + $flags += 'DualMode' + } + + if ($flags.Length -eq 0) { + $flags = [string]::Empty + } + else { + $flags = "[$($flags -join ',')]" + } + + Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow + } + } } \ No newline at end of file diff --git a/src/Private/Events.ps1 b/src/Private/Events.ps1 index 592f51817..fd9872b28 100644 --- a/src/Private/Events.ps1 +++ b/src/Private/Events.ps1 @@ -1,7 +1,7 @@ function Invoke-PodeEvent { param( [Parameter(Mandatory = $true)] - [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')] + [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running','Suspend','Resume','Dump')] [string] $Type ) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 8720d51a9..629b40ab7 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -600,6 +600,25 @@ function Test-PodeDumpPressed { return (Test-PodeKeyPressed -Key $Key -Character 'd') } +function Test-PodeSuspendPressed { + param( + [Parameter()] + $Key = $null + ) + + return (Test-PodeKeyPressed -Key $Key -Character 'u') +} + + +function Test-PodeResumePressed { + param( + [Parameter()] + $Key = $null + ) + + return (Test-PodeKeyPressed -Key $Key -Character 'a') +} + function Test-PodeKeyPressed { param( [Parameter()] @@ -3778,4 +3797,3 @@ function Copy-PodeObjectDeepClone { } - \ No newline at end of file diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index d00073600..4e3bb5e3d 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -2371,4 +2371,68 @@ function Test-PodeRouteOADefinitionTag { return $oaDefinitionTag +} + + +<# +.SYNOPSIS + Displays OpenAPI endpoint information for each definition in Pode. + +.DESCRIPTION + The `Show-PodeOAConsoleInfo` function iterates through the OpenAPI definitions + configured in the Pode server and displays their associated specification and + documentation endpoints in the console. The information includes protocol, address, + and paths for specification and documentation endpoints. + +.EXAMPLE + Show-PodeOAConsoleInfo + + This command will output the OpenAPI information for all definitions currently + configured in the Pode server, including specification and documentation URLs. + +.NOTES + This function uses the `Write-PodeHost` cmdlet to output messages to the console, + with color-coded messages for better readability. + +#> +function Show-PodeOAConsoleInfo { + # state the OpenAPI endpoints for each definition + foreach ($key in $PodeContext.Server.OpenAPI.Definitions.keys) { + $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks + if ( $bookmarks) { + Write-PodeHost + if (!$OpenAPIHeader) { + # OpenAPI Info + Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor Green + $OpenAPIHeader = $true + } + Write-PodeHost " '$key':" -ForegroundColor Yellow + + if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor White + } + # Documentation + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor White + } + } + else { + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow + $PodeContext.Server.EndpointsInfo | ForEach-Object { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) + Write-PodeHost " . $url" -ForegroundColor White + } + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow + $PodeContext.Server.EndpointsInfo | ForEach-Object { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) + Write-PodeHost " . $url" -ForegroundColor White + } + } + } + } } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index e28df41e1..e5b59b189 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -9,7 +9,14 @@ function Start-PodeInternalServer { try { # Check if the running version of Powershell is EOL - Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID))" -ForegroundColor Cyan + Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID)) " -ForegroundColor Cyan -NoNewline + + if($PodeContext.Metrics.Server.RestartCount -gt 0){ + Write-PodeHost "[Restarting]" -ForegroundColor Cyan + }else{ + Write-PodeHost "[Initializing]" -ForegroundColor Cyan + } + $null = Test-PodeVersionPwshEOL -ReportUntested # setup temp drives for internal dirs @@ -83,7 +90,7 @@ function Start-PodeInternalServer { } # start the appropriate server - $endpoints = @() + $PodeContext.Server.EndpointsInfo = @() # - service if ($PodeContext.Server.IsService) { @@ -109,21 +116,21 @@ function Start-PodeInternalServer { foreach ($_type in $PodeContext.Server.Types) { switch ($_type.ToUpperInvariant()) { 'SMTP' { - $endpoints += (Start-PodeSmtpServer) + $PodeContext.Server.EndpointsInfo += (Start-PodeSmtpServer) } 'TCP' { - $endpoints += (Start-PodeTcpServer) + $PodeContext.Server.EndpointsInfo += (Start-PodeTcpServer) } 'HTTP' { - $endpoints += (Start-PodeWebServer -Browse:$Browse) + $PodeContext.Server.EndpointsInfo += (Start-PodeWebServer -Browse:$Browse) } } } # now go back through, and wait for each server type's runspace pool to be ready - foreach ($pool in ($endpoints.Pool | Sort-Object -Unique)) { + foreach ($pool in ($PodeContext.Server.EndpointsInfo.Pool | Sort-Object -Unique)) { $start = [datetime]::Now Write-Verbose "Waiting for the $($pool) RunspacePool to be Ready" @@ -146,80 +153,52 @@ function Start-PodeInternalServer { # run running event hooks Invoke-PodeEvent -Type Running - # state what endpoints are being listened on - if ($endpoints.Length -gt 0) { - - # Listening on the following $endpoints.Length endpoint(s) [$PodeContext.Threads.General thread(s)] - Write-PodeHost ($PodeLocale.listeningOnEndpointsMessage -f $endpoints.Length, $PodeContext.Threads.General) -ForegroundColor Yellow - $endpoints | ForEach-Object { - $flags = @() - if ($_.DualMode) { - $flags += 'DualMode' - } - if ($flags.Length -eq 0) { - $flags = [string]::Empty - } - else { - $flags = "[$($flags -join ',')]" - } + Show-ConsoleInfo -ClearHost -ShowHeader - Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow - } - } - # state the OpenAPI endpoints for each definition - foreach ($key in $PodeContext.Server.OpenAPI.Definitions.keys) { - $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks - if ( $bookmarks) { - Write-PodeHost - if (!$OpenAPIHeader) { - # OpenAPI Info - Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor Green - $OpenAPIHeader = $true - } - Write-PodeHost " '$key':" -ForegroundColor Yellow + } + catch { + throw + } +} - if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { - # Specification - Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow - foreach ($endpoint in $bookmarks.route.Endpoint) { - Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor White - } - # Documentation - Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow - foreach ($endpoint in $bookmarks.route.Endpoint) { - Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor White - } - } - else { - # Specification - Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow - $endpoints | ForEach-Object { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) - Write-PodeHost " . $url" -ForegroundColor White - } - Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow - $endpoints | ForEach-Object { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) - Write-PodeHost " . $url" -ForegroundColor White - } - } - } - } - if (! $PodeContext.Server.DisableTermination) { - Write-PodeHost - Write-PodeHost 'Server Control Commands:' -ForegroundColor Green - Write-PodeHost ' Ctrl+C : Gracefully terminate the server.' -ForegroundColor Cyan - Write-PodeHost ' Ctrl+R : Restart the server and reload configurations.' -ForegroundColor Cyan - if ($PodeContext.Server.Debug.Dump.Enabled) { - Write-PodeHost ' Ctrl+D : Generate a diagnostic dump for debugging purposes.' -ForegroundColor Cyan - } - } +function Show-ConsoleInfo { + param( + [switch] + $ClearHost, + + [switch] + $ShowHeader + ) + if ( $ClearHost ) { + Clear-Host } - catch { - throw + if ($ShowHeader) { + $status = $(if ($PodeContext.Server.Suspended) { 'Suspended' } else { 'Running' }) + Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID)) [$status]" -ForegroundColor Cyan + } + + if (!$PodeContext.Server.Suspended) { + # state what endpoints are being listened on + Show-PodeEndPointConsoleInfo + + # state the OpenAPI endpoints for each definition + Show-PodeOAConsoleInfo + } + + if (! $PodeContext.Server.DisableTermination) { + $resumeOrSuspend = $(if ($PodeContext.Server.Suspended) { 'Resume' } else { 'Suspend' }) + Write-PodeHost + Write-PodeHost 'Server Control Commands:' -ForegroundColor Green + Write-PodeHost ' Ctrl+C : Gracefully terminate the server.' -ForegroundColor Cyan + Write-PodeHost ' Ctrl+R : Restart the server and reload configurations.' -ForegroundColor Cyan + Write-PodeHost " Ctrl+U : $resumeOrSuspend the server." -ForegroundColor Cyan + + if ($PodeContext.Server.Debug.Dump.Enabled) { + Write-PodeHost ' Ctrl+D : Generate a diagnostic dump for debugging purposes.' -ForegroundColor Cyan + } } } @@ -377,4 +356,84 @@ function Test-PodeServerKeepOpen { # keep server open return $true -} \ No newline at end of file +} + +function Suspend-Server { + param( + [int] + $Timeout = 30 + ) + try { + # inform suspend + # Suspending server... + Write-PodeHost 'Suspending server...' -ForegroundColor Cyan + Invoke-PodeEvent -Type Suspend + $PodeContext.Server.Suspended = $true + $runspaces = Get-Runspace -name 'Pode_*' + foreach ($r in $runspaces) { + try { + [Pode.Embedded.DebuggerHandler]::AttachDebugger($r, $false) + # Suspend + Enable-RunspaceDebug -BreakAll -Runspace $r + + Write-PodeHost "Waiting for $($r.Name) to be suspended ." -NoNewLine -ForegroundColor Yellow + + # Initialize the timer + $startTime = [DateTime]::UtcNow + + # Wait for the event to be triggered or timeout + while (! [Pode.Embedded.DebuggerHandler]::IsEventTriggered()) { + Start-Sleep -Milliseconds 1000 + Write-PodeHost '.' -NoNewLine + + if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { + Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" -ForegroundColor Red + return + } + } + Write-PodeHost 'Done' -ForegroundColor Green + } + finally { + [Pode.Embedded.DebuggerHandler]::DetachDebugger($r) + } + + } + start-sleep -seconds 5 + Show-ConsoleInfo -ClearHost -ShowHeader + } + catch { + $_ | Write-PodeErrorLog + } + finally { + Close-PodeDisposable -Disposable $PodeContext.Tokens.SuspendResume + $PodeContext.Tokens.SuspendResume = [System.Threading.CancellationTokenSource]::new() + } +} + + +function Resume-Server { + try { + # inform resume + # Resuming server... + Write-PodeHost 'Resuming server...' -NoNewline -ForegroundColor Cyan + + Invoke-PodeEvent -Type Resume + $PodeContext.Server.Suspended = $false + Start-Sleep 5 + $runspaces = Get-Runspace -name 'Pode_*' + foreach ($r in $runspaces) { + # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. + Disable-RunspaceDebug -Runspace $r + } + Write-PodeHost 'Done' -ForegroundColor Green + Start-Sleep 1 + Show-ConsoleInfo -ClearHost -ShowHeader + } + finally { + Close-PodeDisposable -Disposable $PodeContext.Tokens.SuspendResume + $PodeContext.Tokens.SuspendResume = [System.Threading.CancellationTokenSource]::new() + } + +} + + diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index f6c4706be..c34ffa714 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -146,6 +146,9 @@ function Start-PodeServer { # Sets the name of the current runspace Set-PodeCurrentRunspaceName -Name 'PodeServer' + # Compile the Debug Handler + Initialize-DebugHandler + # ensure the session is clean $Script:PodeContext = $null $ShowDoneMessage = $true @@ -208,7 +211,7 @@ function Start-PodeServer { # get the next key presses $key = Get-PodeConsoleKey - + # check for internal restart if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { Restart-PodeInternalServer @@ -222,6 +225,16 @@ function Start-PodeServer { } } + + if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { + if ( $PodeContext.Server.Suspended) { + Resume-Server + } + else { + Suspend-Server + } + } + # check for open browser if (Test-PodeOpenBrowserPressed -Key $key) { Invoke-PodeEvent -Type Browser diff --git a/src/Public/Runspaces.ps1 b/src/Public/Runspaces.ps1 index 286a0aacd..167a2142f 100644 --- a/src/Public/Runspaces.ps1 +++ b/src/Public/Runspaces.ps1 @@ -11,7 +11,7 @@ .EXAMPLE Set-PodeCurrentRunspaceName -Name "MyRunspace" - This command sets the name of the current runspace to "MyRunspace". + This command sets the name of the current runspace to "Pode_MyRunspace". .NOTES This is an internal function and may change in future releases of Pode. @@ -27,6 +27,11 @@ function Set-PodeCurrentRunspaceName { # Get the current runspace $currentRunspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace + + if (!$Name.StartsWith( 'Pode_' ) -and $Name -ne 'PodeServer') { + $Name = 'Pode_' + $Name + } + # Set the name of the current runspace if the name is not already set if ( $currentRunspace.Name -ne $Name) { # Set the name of the current runspace From 7db258a01837421643bf648c90fb96580a154e0d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 5 Nov 2024 13:22:53 -0800 Subject: [PATCH 09/34] internazioanalization+ example --- examples/PetStore/Petstore-OpenApi.ps1 | 12 -- examples/Web-Dump.ps1 | 91 +++++++++++++ src/Locales/ar/Pode.psd1 | 14 +- src/Locales/de/Pode.psd1 | 14 +- src/Locales/en-us/Pode.psd1 | 15 +- src/Locales/en/Pode.psd1 | 14 +- src/Locales/es/Pode.psd1 | 14 +- src/Locales/fr/Pode.psd1 | 14 +- src/Locales/it/Pode.psd1 | 14 +- src/Locales/ja/Pode.psd1 | 14 +- src/Locales/ko/Pode.psd1 | 14 +- src/Locales/nl/Pode.psd1 | 14 +- src/Locales/pl/Pode.psd1 | 14 +- src/Locales/pt/Pode.psd1 | 14 +- src/Locales/zh/Pode.psd1 | 14 +- src/Pode.psd1 | 2 + src/Private/Dump.ps1 | 7 +- src/Private/OpenApi.ps1 | 4 +- src/Private/Server.ps1 | 181 +++++++++++++++++++------ src/Public/Core.ps1 | 54 +++++++- src/Public/OpenApi.ps1 | 2 +- src/Public/Utilities.ps1 | 4 - 22 files changed, 455 insertions(+), 85 deletions(-) create mode 100644 examples/Web-Dump.ps1 diff --git a/examples/PetStore/Petstore-OpenApi.ps1 b/examples/PetStore/Petstore-OpenApi.ps1 index 7a298cdae..ed78f7f96 100644 --- a/examples/PetStore/Petstore-OpenApi.ps1 +++ b/examples/PetStore/Petstore-OpenApi.ps1 @@ -895,17 +895,5 @@ Some useful links: Set-PodeOARequest -Parameters ( New-PodeOAStringProperty -Name 'username' -Description 'The name that needs to be deleted.' -Required | ConvertTo-PodeOAParameter -In Path ) -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' -PassThru | Add-PodeOAResponse -StatusCode 404 -Description 'User not found' - - Add-PodeRoute -PassThru -Method Get -Path '/dump' -ScriptBlock { - $format = $WebEvent.Query['format'] - try { - # Simulate a critical error - throw [System.DivideByZeroException] 'Simulated divide by zero error' - } - catch { - $_ | Invoke-PodeDump -Format $format - } - } | Set-PodeOARouteInfo -Summary 'Dump state' -Description 'Dump the memory state of the server.' -Tags 'dump' -OperationId 'dump'-PassThru | - Set-PodeOARequest -Parameters (New-PodeOAStringProperty -Name 'format' -Description 'Dump export format.' -Enum 'json', 'clixml', 'txt', 'bin', 'yaml' -Default 'json' | ConvertTo-PodeOAParameter -In Query ) } } \ No newline at end of file diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 new file mode 100644 index 000000000..2ecaa9e5d --- /dev/null +++ b/examples/Web-Dump.ps1 @@ -0,0 +1,91 @@ +<# +.SYNOPSIS + Initializes and starts a Pode server with OpenAPI support and error logging. + +.DESCRIPTION + This script sets up a Pode server with HTTP endpoints, error logging, and OpenAPI documentation. + It also includes a sample route to simulate a critical error and dump the server's memory state. + + .EXAMPLE + To run the sample: ./Web-Dump.ps1 + + OpenAPI Info: + Specification: + http://localhost:8081/openapi + Documentation: + http://localhost:8081/docs + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Web-Dump.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # 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 Pode server with specified script block +Start-PodeServer -Threads 1 -ScriptBlock { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + + # set view engine to pode renderer + Set-PodeViewEngine -Type Pode + + # Enable error logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + + # Enable OpenAPI documentation + + Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions -NoDefaultResponses + + Add-PodeOAInfo -Title 'Dump - OpenAPI 3.0.3' -Version 1.0.1 + Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' + + # Enable OpenAPI viewers + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOAViewer -Type ReDoc -Path '/docs/redoc' -DarkMode + Enable-PodeOAViewer -Type RapiDoc -Path '/docs/rapidoc' -DarkMode + Enable-PodeOAViewer -Type StopLight -Path '/docs/stoplight' -DarkMode + Enable-PodeOAViewer -Type Explorer -Path '/docs/explorer' -DarkMode + Enable-PodeOAViewer -Type RapiPdf -Path '/docs/rapipdf' -DarkMode + + # Enable OpenAPI editor and bookmarks + Enable-PodeOAViewer -Editor -Path '/docs/swagger-editor' + Enable-PodeOAViewer -Bookmarks -Path '/docs' + + # Setup session details + Enable-PodeSessionMiddleware -Duration 120 -Extend + + # Define API routes + Add-PodeRouteGroup -Path '/api/v3' -Routes { + + Add-PodeRoute -PassThru -Method Get -Path '/dump' -ScriptBlock { + $format = $WebEvent.Query['format'] + try { + # Simulate a critical error + throw [System.DivideByZeroException] 'Simulated divide by zero error' + } + catch { + $_ | Invoke-PodeDump -Format $format + } + } | Set-PodeOARouteInfo -Summary 'Dump state' -Description 'Dump the memory state of the server.' -Tags 'dump' -OperationId 'dump'-PassThru | + Set-PodeOARequest -Parameters (New-PodeOAStringProperty -Name 'format' -Description 'Dump export format.' -Enum 'json', 'clixml', 'txt', 'bin', 'yaml' -Default 'json' | ConvertTo-PodeOAParameter -In Query ) + } +} \ No newline at end of file diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 1c7ff7daf..3bc5c3a52 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' لا يمكن أن يحتوي على جسم الطلب. استخدم -AllowNonStandardBody لتجاوز هذا التقييد." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' - LocalEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API." + localEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API." + suspendingMessage = 'تعليق الخادم...' + resumingMessage = 'استئناف الخادم...' + serverControlCommandsTitle = 'أوامر التحكم بالخادم:' + gracefullyTerminateMessage = 'إنهاء الخادم بلطف.' + restartServerMessage = 'إعادة تشغيل الخادم وتحميل التكوينات.' + resumeServerMessage = 'استئناف الخادم.' + suspendServerMessage = 'تعليق الخادم.' + generateDiagnosticDumpMessage = 'إنشاء تفريغ تشخيصي لأغراض التصحيح.' + initializingMessage = 'جارٍ التهيئة' + restartingMessage = 'جارٍ إعادة التشغيل' + suspendedMessage = 'معلق' + runningMessage = 'يعمل' } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index c90847e01..aa62fe4d3 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' Operationen dürfen keinen Anfragekörper haben. Verwenden Sie -AllowNonStandardBody, um diese Einschränkung zu umgehen." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe." unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}' - LocalEndpointConflictExceptionMessage = "Sowohl '{0}' als auch '{1}' sind als lokale OpenAPI-Endpunkte definiert, aber es ist nur ein lokaler Endpunkt pro API-Definition erlaubt." + localEndpointConflictExceptionMessage = "Sowohl '{0}' als auch '{1}' sind als lokale OpenAPI-Endpunkte definiert, aber es ist nur ein lokaler Endpunkt pro API-Definition erlaubt." + suspendingMessage = 'Server wird angehalten...' + resumingMessage = 'Server wird fortgesetzt...' + serverControlCommandsTitle = 'Serversteuerbefehle:' + gracefullyTerminateMessage = 'Server sanft beenden.' + restartServerMessage = 'Server neu starten und Konfigurationen laden.' + resumeServerMessage = 'Server fortsetzen.' + suspendServerMessage = 'Server anhalten.' + generateDiagnosticDumpMessage = 'Diagnostischen Dump für Debuggingzwecke erstellen.' + initializingMessage = 'Initialisierung' + restartingMessage = 'Neustart' + suspendedMessage = 'Angehalten' + runningMessage = 'Läuft' } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 56340e18f..cfd715ac4 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -291,4 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' - LocalEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition."} \ No newline at end of file + localEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition." + suspendingMessage = 'Suspending server...' + resumingMessage = 'Resuming server...' + serverControlCommandsTitle = 'Server Control Commands:' + gracefullyTerminateMessage = 'Gracefully terminate the server.' + restartServerMessage = 'Restart the server and reload configurations.' + resumeServerMessage = 'Resume the server.' + suspendServerMessage = 'Suspend the server.' + generateDiagnosticDumpMessage = 'Generate a diagnostic dump for debugging purposes.' + initializingMessage = 'Initializing' + restartingMessage = 'Restarting' + suspendedMessage = 'Suspended' + runningMessage = 'Running' +} \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 44a1ee102..11a927c14 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' - LocalEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition." + localEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition." + suspendingMessage = 'Suspending server...' + resumingMessage = 'Resuming server...' + serverControlCommandsTitle = 'Server Control Commands:' + gracefullyTerminateMessage = 'Gracefully terminate the server.' + restartServerMessage = 'Restart the server and reload configurations.' + resumeServerMessage = 'Resume the server.' + suspendServerMessage = 'Suspend the server.' + generateDiagnosticDumpMessage = 'Generate a diagnostic dump for debugging purposes.' + initializingMessage = 'Initialising' + restartingMessage = 'Restarting' + suspendedMessage = 'Suspended' + runningMessage = 'Running' } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 9c2ee1194..06402b4ca 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "Las operaciones '{0}' no pueden tener un cuerpo de solicitud. Use -AllowNonStandardBody para evitar esta restricción." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La función '{0}' no acepta una matriz como entrada de canalización." unsupportedStreamCompressionEncodingExceptionMessage = 'La codificación de compresión de transmisión no es compatible: {0}' - LocalEndpointConflictExceptionMessage = "Tanto '{0}' como '{1}' están definidos como puntos finales locales de OpenAPI, pero solo se permite un punto final local por definición de API." + localEndpointConflictExceptionMessage = "Tanto '{0}' como '{1}' están definidos como puntos finales locales de OpenAPI, pero solo se permite un punto final local por definición de API." + suspendingMessage = 'Suspendiendo el servidor...' + resumingMessage = 'Reanudando el servidor...' + serverControlCommandsTitle = 'Comandos de control del servidor:' + gracefullyTerminateMessage = 'Terminar el servidor de manera ordenada.' + restartServerMessage = 'Reiniciar el servidor y recargar configuraciones.' + resumeServerMessage = 'Reanudar el servidor.' + suspendServerMessage = 'Suspender el servidor.' + generateDiagnosticDumpMessage = 'Generar un volcado de diagnóstico para fines de depuración.' + initializingMessage = 'Inicializando' + restartingMessage = 'Reiniciando' + suspendedMessage = 'Suspendido' + runningMessage = 'En ejecución' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 2c9dfc579..1509974e9 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "Les opérations '{0}' ne peuvent pas avoir de corps de requête. Utilisez -AllowNonStandardBody pour contourner cette restriction." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La fonction '{0}' n'accepte pas un tableau en tant qu'entrée de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = "La compression de flux {0} n'est pas prise en charge." - LocalEndpointConflictExceptionMessage = "Les deux '{0}' et '{1}' sont définis comme des points de terminaison locaux pour OpenAPI, mais un seul point de terminaison local est autorisé par définition d'API." + localEndpointConflictExceptionMessage = "Les deux '{0}' et '{1}' sont définis comme des points de terminaison locaux pour OpenAPI, mais un seul point de terminaison local est autorisé par définition d'API." + suspendingMessage = 'Suspension du serveur...' + resumingMessage = 'Reprise du serveur...' + serverControlCommandsTitle = 'Commandes de contrôle du serveur :' + gracefullyTerminateMessage = 'Arrêter le serveur gracieusement.' + restartServerMessage = 'Redémarrer le serveur et recharger les configurations.' + resumeServerMessage = 'Reprendre le serveur.' + suspendServerMessage = 'Suspendre le serveur.' + generateDiagnosticDumpMessage = 'Générer un dump diagnostique à des fins de débogage.' + initializingMessage = 'Initialisation' + restartingMessage = 'Redémarrage' + suspendedMessage = 'Suspendu' + runningMessage = "En cours d'exécution" } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 999bf85c3..7ce844e66 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "Le operazioni '{0}' non possono avere un corpo della richiesta. Utilizzare -AllowNonStandardBody per aggirare questa restrizione." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non è supportata per la codifica {0}' - LocalEndpointConflictExceptionMessage = "Sia '{0}' che '{1}' sono definiti come endpoint locali OpenAPI, ma è consentito solo un endpoint locale per definizione API." + localEndpointConflictExceptionMessage = "Sia '{0}' che '{1}' sono definiti come endpoint locali OpenAPI, ma è consentito solo un endpoint locale per definizione API." + suspendingMessage = 'Sospensione del server...' + resumingMessage = 'Ripresa del server...' + serverControlCommandsTitle = 'Comandi di controllo del server:' + gracefullyTerminateMessage = 'Termina il server con grazia.' + restartServerMessage = 'Riavviare il server e ricaricare le configurazioni.' + resumeServerMessage = 'Riprendi il server.' + suspendServerMessage = 'Sospendi il server.' + generateDiagnosticDumpMessage = 'Genera un dump diagnostico a scopo di debug.' + initializingMessage = 'Inizializzazione' + restartingMessage = 'Riavvio' + suspendedMessage = 'Sospeso' + runningMessage = 'In esecuzione' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index e65627c59..922beb8e3 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' 操作にはリクエストボディを含めることはできません。-AllowNonStandardBody を使用してこの制限を回避してください。" fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。" unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' - LocalEndpointConflictExceptionMessage = "'{0}' と '{1}' は OpenAPI のローカルエンドポイントとして定義されていますが、API 定義ごとに 1 つのローカルエンドポイントのみ許可されます。" + localEndpointConflictExceptionMessage = "'{0}' と '{1}' は OpenAPI のローカルエンドポイントとして定義されていますが、API 定義ごとに 1 つのローカルエンドポイントのみ許可されます。" + suspendingMessage = 'サーバーを一時停止しています...' + resumingMessage = 'サーバーを再開しています...' + serverControlCommandsTitle = 'サーバーコントロールコマンド:' + gracefullyTerminateMessage = 'サーバーを正常に終了します。' + restartServerMessage = 'サーバーを再起動して設定をリロードします。' + resumeServerMessage = 'サーバーを再開します。' + suspendServerMessage = 'サーバーを一時停止します。' + generateDiagnosticDumpMessage = 'デバッグ目的の診断ダンプを生成します。' + initializingMessage = '初期化中' + restartingMessage = '再起動中' + suspendedMessage = '一時停止中' + runningMessage = '実行中' } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index f64f0c61f..1e489607e 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' 작업은 요청 본문을 가질 수 없습니다. 이 제한을 무시하려면 -AllowNonStandardBody를 사용하세요." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' - LocalEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." + localEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." + suspendingMessage = '서버를 일시 중지하는 중...' + resumingMessage = '서버를 재개하는 중...' + serverControlCommandsTitle = '서버 제어 명령:' + gracefullyTerminateMessage = '서버를 정상적으로 종료합니다.' + restartServerMessage = '서버를 재시작하고 설정을 다시 로드합니다.' + resumeServerMessage = '서버를 재개합니다.' + suspendServerMessage = '서버를 일시 중지합니다.' + generateDiagnosticDumpMessage = '디버깅을 위한 진단 덤프를 생성합니다.' + initializingMessage = '초기화 중' + restartingMessage = '재시작 중' + suspendedMessage = '일시 중지됨' + runningMessage = '실행 중' } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index d7933a0d9..1b07fbccd 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' operaties kunnen geen aanvraagbody hebben. Gebruik -AllowNonStandardBody om deze beperking te omzeilen." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer." unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}' - LocalEndpointConflictExceptionMessage = "Zowel '{0}' als '{1}' zijn gedefinieerd als lokale OpenAPI-eindpunten, maar er is slechts één lokaal eindpunt per API-definitie toegestaan." + localEndpointConflictExceptionMessage = "Zowel '{0}' als '{1}' zijn gedefinieerd als lokale OpenAPI-eindpunten, maar er is slechts één lokaal eindpunt per API-definitie toegestaan." + suspendingMessage = 'Server wordt gepauzeerd...' + resumingMessage = 'Server wordt hervat...' + serverControlCommandsTitle = "Serverbedieningscommando's:" + gracefullyTerminateMessage = 'Server netjes afsluiten.' + restartServerMessage = 'Server opnieuw starten en configuraties herladen.' + resumeServerMessage = 'Server hervatten.' + suspendServerMessage = 'Server pauzeren.' + generateDiagnosticDumpMessage = 'Genereer een diagnostische dump voor debuggingdoeleinden.' + initializingMessage = 'Initialiseren' + restartingMessage = 'Herstarten' + suspendedMessage = 'Gepauzeerd' + runningMessage = 'Actief' } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index cd632c469..2b295321f 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "Operacje '{0}' nie mogą zawierać treści żądania. Użyj -AllowNonStandardBody, aby obejść to ograniczenie." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku." unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}' - LocalEndpointConflictExceptionMessage = "Zarówno '{0}', jak i '{1}' są zdefiniowane jako lokalne punkty końcowe OpenAPI, ale na jedną definicję API dozwolony jest tylko jeden lokalny punkt końcowy." + localEndpointConflictExceptionMessage = "Zarówno '{0}', jak i '{1}' są zdefiniowane jako lokalne punkty końcowe OpenAPI, ale na jedną definicję API dozwolony jest tylko jeden lokalny punkt końcowy." + suspendingMessage = 'Wstrzymywanie serwera...' + resumingMessage = 'Wznawianie serwera...' + serverControlCommandsTitle = 'Polecenia sterowania serwerem:' + gracefullyTerminateMessage = 'Łagodne zakończenie działania serwera.' + restartServerMessage = 'Ponowne uruchomienie serwera i załadowanie konfiguracji.' + resumeServerMessage = 'Wznowienie serwera.' + suspendServerMessage = 'Wstrzymanie serwera.' + generateDiagnosticDumpMessage = 'Wygeneruj zrzut diagnostyczny do celów debugowania.' + initializingMessage = 'Inicjalizacja' + restartingMessage = 'Ponowne uruchamianie' + suspendedMessage = 'Wstrzymany' + runningMessage = 'Działa' } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index a0604c179..9b2412f5c 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "As operações '{0}' não podem ter um corpo de solicitação. Use -AllowNonStandardBody para contornar essa restrição." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "A função '{0}' não aceita uma matriz como entrada de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'A codificação de compressão de fluxo não é suportada.' - LocalEndpointConflictExceptionMessage = "Tanto '{0}' quanto '{1}' estão definidos como endpoints locais do OpenAPI, mas apenas um endpoint local é permitido por definição de API." + localEndpointConflictExceptionMessage = "Tanto '{0}' quanto '{1}' estão definidos como endpoints locais do OpenAPI, mas apenas um endpoint local é permitido por definição de API." + suspendingMessage = 'Suspendendo o servidor...' + resumingMessage = 'Retomando o servidor...' + serverControlCommandsTitle = 'Comandos de controle do servidor:' + gracefullyTerminateMessage = 'Encerrar o servidor graciosamente.' + restartServerMessage = 'Reiniciar o servidor e recarregar configurações.' + resumeServerMessage = 'Retomar o servidor.' + suspendServerMessage = 'Suspender o servidor.' + generateDiagnosticDumpMessage = 'Gerar um despejo de diagnóstico para fins de depuração.' + initializingMessage = 'Inicializando' + restartingMessage = 'Reiniciando' + suspendedMessage = 'Suspenso' + runningMessage = 'Executando' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 26b013c95..cf708346c 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -291,5 +291,17 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' 操作无法包含请求体。使用 -AllowNonStandardBody 以解除此限制。" fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' - LocalEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" + localEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" + suspendingMessage = '正在暂停服务器...' + resumingMessage = '正在恢复服务器...' + serverControlCommandsTitle = '服务器控制命令:' + gracefullyTerminateMessage = '正常终止服务器。' + restartServerMessage = '重启服务器并重新加载配置。' + resumeServerMessage = '恢复服务器。' + suspendServerMessage = '暂停服务器。' + generateDiagnosticDumpMessage = '生成诊断转储以用于调试。' + initializingMessage = '初始化中' + restartingMessage = '正在重启' + suspendedMessage = '已暂停' + runningMessage = '运行中' } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 34c63c00b..caf67a0c0 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -308,6 +308,8 @@ 'Get-PodeServerDefaultSecret', 'Wait-PodeDebugger', 'Get-PodeVersion', + 'Suspend-PodeServer', + 'Resume-PodeServer', # openapi 'Enable-PodeOpenApi', diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 8edb66461..d270de7be 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -363,9 +363,6 @@ function Get-PodeDumpScopedVariable { [int] $MaxDepth = 5 ) - - - # Collect variables from Local, Script, and Global scopes $scopes = @{ Local = Get-Variable -Scope 0 @@ -487,9 +484,7 @@ function ConvertTo-PodeSerializableObject { } } -function Initialize-DebugHandler { - - +function Initialize-PodeDebugHandler { # Embed C# code to handle the DebuggerStop event Add-Type @' using System; diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index 4e3bb5e3d..493a12f79 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -2391,9 +2391,7 @@ function Test-PodeRouteOADefinitionTag { configured in the Pode server, including specification and documentation URLs. .NOTES - This function uses the `Write-PodeHost` cmdlet to output messages to the console, - with color-coded messages for better readability. - + This is an internal function and may change in future releases of Pode. #> function Show-PodeOAConsoleInfo { # state the OpenAPI endpoints for each definition diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index e5b59b189..2213c5099 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -11,10 +11,11 @@ function Start-PodeInternalServer { # Check if the running version of Powershell is EOL Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID)) " -ForegroundColor Cyan -NoNewline - if($PodeContext.Metrics.Server.RestartCount -gt 0){ - Write-PodeHost "[Restarting]" -ForegroundColor Cyan - }else{ - Write-PodeHost "[Initializing]" -ForegroundColor Cyan + if ($PodeContext.Metrics.Server.RestartCount -gt 0) { + Write-PodeHost "[$( $PodeLocale.restartingMessage)]" -ForegroundColor Cyan + } + else { + Write-PodeHost "[$($PodeLocale.initializingMessage)]" -ForegroundColor Cyan } $null = Test-PodeVersionPwshEOL -ReportUntested @@ -154,7 +155,7 @@ function Start-PodeInternalServer { # run running event hooks Invoke-PodeEvent -Type Running - Show-ConsoleInfo -ClearHost -ShowHeader + Show-PodeConsoleInfo -ClearHost -ShowHeader } catch { @@ -162,8 +163,26 @@ function Start-PodeInternalServer { } } +<# +.SYNOPSIS + Displays Pode server information on the console, including version, PID, status, endpoints, and control commands. + +.DESCRIPTION + The Show-PodeConsoleInfo function displays key information about the current Pode server instance. + It optionally clears the console before displaying server details such as version, process ID (PID), and running status. + If the server is running, it also displays information about active endpoints and OpenAPI definitions. + Additionally, it provides server control commands like restart, suspend, and generating diagnostic dumps. -function Show-ConsoleInfo { +.PARAMETER ClearHost + Clears the console screen before displaying server information. + +.PARAMETER ShowHeader + Displays the Pode version, server process ID (PID), and current server status in the console header. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Show-PodeConsoleInfo { param( [switch] $ClearHost, @@ -172,11 +191,17 @@ function Show-ConsoleInfo { $ShowHeader ) - if ( $ClearHost ) { + if ($ClearHost) { Clear-Host } if ($ShowHeader) { - $status = $(if ($PodeContext.Server.Suspended) { 'Suspended' } else { 'Running' }) + + if ($PodeContext.Server.Suspended) { + $status = $Podelocale.suspendedMessage # Suspended + } + else { + $status = $Podelocale.runningMessage # Running + } Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID)) [$status]" -ForegroundColor Cyan } @@ -188,16 +213,16 @@ function Show-ConsoleInfo { Show-PodeOAConsoleInfo } - if (! $PodeContext.Server.DisableTermination) { - $resumeOrSuspend = $(if ($PodeContext.Server.Suspended) { 'Resume' } else { 'Suspend' }) + if (!$PodeContext.Server.DisableTermination) { + $resumeOrSuspend = $(if ($PodeContext.Server.Suspended) { $Podelocale.ResumeServerMessage } else { $Podelocale.SuspendServerMessage }) Write-PodeHost - Write-PodeHost 'Server Control Commands:' -ForegroundColor Green - Write-PodeHost ' Ctrl+C : Gracefully terminate the server.' -ForegroundColor Cyan - Write-PodeHost ' Ctrl+R : Restart the server and reload configurations.' -ForegroundColor Cyan - Write-PodeHost " Ctrl+U : $resumeOrSuspend the server." -ForegroundColor Cyan + Write-PodeHost $Podelocale.ServerControlCommandsTitle -ForegroundColor Green + Write-PodeHost " Ctrl+C : $($Podelocale.GracefullyTerminateMessage)" -ForegroundColor Cyan + Write-PodeHost " Ctrl+R : $($Podelocale.RestartServerMessage)" -ForegroundColor Cyan + Write-PodeHost " Ctrl+U : $resumeOrSuspend" -ForegroundColor Cyan if ($PodeContext.Server.Debug.Dump.Enabled) { - Write-PodeHost ' Ctrl+D : Generate a diagnostic dump for debugging purposes.' -ForegroundColor Cyan + Write-PodeHost " Ctrl+D : $($Podelocale.GenerateDiagnosticDumpMessage)" -ForegroundColor Cyan } } } @@ -206,7 +231,7 @@ function Restart-PodeInternalServer { try { # inform restart # Restarting server... - Write-PodeHost $PodeLocale.restartingServerMessage -NoNewline -ForegroundColor Cyan + Write-PodeHost $PodeLocale.restartingServerMessage -NoNewline -ForegroundColor Yellow # run restart event hooks Invoke-PodeEvent -Type Restart @@ -358,82 +383,154 @@ function Test-PodeServerKeepOpen { return $true } -function Suspend-Server { +<# +.SYNOPSIS + Suspends the Pode server and its runspaces. + +.DESCRIPTION + This function suspends the Pode server by pausing all associated runspaces and ensuring they enter a debug state. + It triggers the 'Suspend' event, updates the server's suspended status, and provides feedback during the suspension process. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for each runspace to be suspended before timing out. Default is 30 seconds. + +.NOTES + This is an internal function used within the Pode framework. + It may change in future releases. + +.EXAMPLE + Suspend-PodeServerInternal -Timeout 60 + # Suspends the Pode server with a timeout of 60 seconds. + +#> +function Suspend-PodeServerInternal { param( [int] $Timeout = 30 ) try { - # inform suspend - # Suspending server... - Write-PodeHost 'Suspending server...' -ForegroundColor Cyan + # Inform user that the server is suspending + Write-PodeHost $PodeLocale.SuspendingMessage -ForegroundColor Yellow + + # Trigger the Suspend event Invoke-PodeEvent -Type Suspend + + # Update the server's suspended state $PodeContext.Server.Suspended = $true + + # Retrieve all runspaces related to Pode $runspaces = Get-Runspace -name 'Pode_*' - foreach ($r in $runspaces) { + foreach ($runspace in $runspaces) { try { - [Pode.Embedded.DebuggerHandler]::AttachDebugger($r, $false) - # Suspend - Enable-RunspaceDebug -BreakAll -Runspace $r + # Attach debugger to the runspace + [Pode.Embedded.DebuggerHandler]::AttachDebugger($runspace, $false) + + # Enable debugging and pause execution + Enable-RunspaceDebug -BreakAll -Runspace $runspace - Write-PodeHost "Waiting for $($r.Name) to be suspended ." -NoNewLine -ForegroundColor Yellow + # Inform user about the suspension process for the current runspace + Write-PodeHost "Waiting for $($runspace.Name) to be suspended." -NoNewLine -ForegroundColor Yellow # Initialize the timer $startTime = [DateTime]::UtcNow - # Wait for the event to be triggered or timeout + # Wait for the suspension event or until timeout while (! [Pode.Embedded.DebuggerHandler]::IsEventTriggered()) { Start-Sleep -Milliseconds 1000 Write-PodeHost '.' -NoNewLine + # Check for timeout if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" -ForegroundColor Red return } } + + # Inform user that the suspension is complete Write-PodeHost 'Done' -ForegroundColor Green } finally { - [Pode.Embedded.DebuggerHandler]::DetachDebugger($r) + # Detach the debugger from the runspace + [Pode.Embedded.DebuggerHandler]::DetachDebugger($runspace) } - } - start-sleep -seconds 5 - Show-ConsoleInfo -ClearHost -ShowHeader + + # Short pause before refreshing the console + Start-Sleep -Seconds 5 + + # Clear the host and display header information + Show-PodeConsoleInfo -ClearHost -ShowHeader } catch { + # Log any errors that occur $_ | Write-PodeErrorLog } finally { + # Ensure cleanup of disposable tokens Close-PodeDisposable -Disposable $PodeContext.Tokens.SuspendResume + + # Reinitialize the CancellationTokenSource for future suspension/resumption $PodeContext.Tokens.SuspendResume = [System.Threading.CancellationTokenSource]::new() } } -function Resume-Server { +<# +.SYNOPSIS + Resumes the Pode server from a suspended state. + +.DESCRIPTION + This function resumes the Pode server, ensuring all associated runspaces are restored to their normal execution state. + It triggers the 'Resume' event, updates the server's suspended status, and clears the host for a refreshed console view. + +.NOTES + This is an internal function used within the Pode framework. + It may change in future releases. + +.EXAMPLE + Resume-PodeServerInternal + # Resumes the Pode server after a suspension. + +#> +function Resume-PodeServerInternal { try { - # inform resume - # Resuming server... - Write-PodeHost 'Resuming server...' -NoNewline -ForegroundColor Cyan + # Inform user that the server is resuming + Write-PodeHost $PodeLocale.ResumingMessage -NoNewline -ForegroundColor Yellow + # Trigger the Resume event Invoke-PodeEvent -Type Resume + + # Update the server's suspended state $PodeContext.Server.Suspended = $false + + # Pause briefly to ensure any required internal processes have time to stabilize Start-Sleep 5 + + # Retrieve all runspaces related to Pode $runspaces = Get-Runspace -name 'Pode_*' - foreach ($r in $runspaces) { - # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. - Disable-RunspaceDebug -Runspace $r + foreach ($runspace in $runspaces) { + # Disable debugging for each runspace to restore normal execution + Disable-RunspaceDebug -Runspace $runspace } + + # Inform user that the resume process is complete Write-PodeHost 'Done' -ForegroundColor Green + + # Small delay before refreshing the console Start-Sleep 1 - Show-ConsoleInfo -ClearHost -ShowHeader + + # Clear the host and display header information + Show-PodeConsoleInfo -ClearHost -ShowHeader + } + catch { + # Log any errors that occur + $_ | Write-PodeErrorLog } finally { + # Ensure cleanup of disposable tokens Close-PodeDisposable -Disposable $PodeContext.Tokens.SuspendResume + + # Reinitialize the CancellationTokenSource for future suspension/resumption $PodeContext.Tokens.SuspendResume = [System.Threading.CancellationTokenSource]::new() } - -} - - +} \ No newline at end of file diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index c34ffa714..3397b3019 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -147,7 +147,7 @@ function Start-PodeServer { Set-PodeCurrentRunspaceName -Name 'PodeServer' # Compile the Debug Handler - Initialize-DebugHandler + Initialize-PodeDebugHandler # ensure the session is clean $Script:PodeContext = $null @@ -225,13 +225,12 @@ function Start-PodeServer { } } - if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { if ( $PodeContext.Server.Suspended) { - Resume-Server + Resume-PodeServerInternal } else { - Suspend-Server + Suspend-PodeServerInternal } } @@ -318,6 +317,53 @@ function Restart-PodeServer { $PodeContext.Tokens.Restart.Cancel() } + +<# +.SYNOPSIS + Resumes the Pode server from a suspended state. + +.DESCRIPTION + This function resumes the Pode server, ensuring all associated runspaces are restored to their normal execution state. + It triggers the 'Resume' event, updates the server's suspended status, and clears the host for a refreshed console view. + +.EXAMPLE + Resume-PodeServer + # Resumes the Pode server after a suspension. + +#> +function Resume-PodeServer { + [CmdletBinding()] + param() + if ( $PodeContext.Server.Suspended) { + $PodeContext.Tokens.SuspendResume.Cancel() + } +} + + +<# +.SYNOPSIS + Suspends the Pode server and its runspaces. + +.DESCRIPTION + This function suspends the Pode server by pausing all associated runspaces and ensuring they enter a debug state. + It triggers the 'Suspend' event, updates the server's suspended status, and provides feedback during the suspension process. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for each runspace to be suspended before timing out. Default is 30 seconds. + +.EXAMPLE + Suspend-PodeServerInternal -Timeout 60 + # Suspends the Pode server with a timeout of 60 seconds. + +#> +function Suspend-PodeServer { + [CmdletBinding()] + param() + if (! $PodeContext.Server.Suspended) { + $PodeContext.Tokens.SuspendResume.Cancel() + } +} + <# .SYNOPSIS Helper wrapper function to start a Pode web server for a static website at the current directory. diff --git a/src/Public/OpenApi.ps1 b/src/Public/OpenApi.ps1 index 3c3940b32..6fb6c5db4 100644 --- a/src/Public/OpenApi.ps1 +++ b/src/Public/OpenApi.ps1 @@ -428,7 +428,7 @@ function Add-PodeOAServerEndpoint { # If there's already a local endpoint, throw an exception, as only one local endpoint is allowed per definition # Both are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition. if ($srv.url -notmatch '^(?i)https?://') { - throw ($PodeLocale.LocalEndpointConflictExceptionMessage -f $Url, $srv.url) + throw ($PodeLocale.localEndpointConflictExceptionMessage -f $Url, $srv.url) } } } diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index a758f7af6..a33f2e604 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1487,9 +1487,6 @@ function Invoke-PodeGC { [System.GC]::Collect() } - - - <# .SYNOPSIS Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. @@ -1565,7 +1562,6 @@ function Invoke-PodeDump { [int] $MaxDepth ) - write-podehost -explode $PSBoundParameters $PodeContext.Server.Debug.Dump.Param = $PSBoundParameters $PodeContext.Tokens.Dump.Cancel() } \ No newline at end of file From 88642e80845efccd0236d1d0e0667431e62a1cbb Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 5 Nov 2024 13:26:28 -0800 Subject: [PATCH 10/34] Update PetData.json --- examples/PetStore/data/PetData.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/examples/PetStore/data/PetData.json b/examples/PetStore/data/PetData.json index 0f8f81e79..1f5fe3cd8 100644 --- a/examples/PetStore/data/PetData.json +++ b/examples/PetStore/data/PetData.json @@ -43,14 +43,6 @@ "petId": 1, "id": 2, "quantity": 50 - }, - "10": { - "id": 10, - "petId": 198772, - "quantity": 7, - "shipDate": "2024-11-04T02:08:54.351Z", - "status": "approved", - "complete": true } }, "Scope": [ @@ -368,4 +360,4 @@ "Users" ] } -} +} \ No newline at end of file From 65836b55e46d485d516456ca0b9efe765ca8d19b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 5 Nov 2024 18:01:19 -0800 Subject: [PATCH 11/34] Documentation --- docs/Getting-Started/Debug.md | 11 ++++ .../Restarting/Overview.md | 2 + .../Restarting/Type}/AutoRestarting.md | 0 .../Restarting/Type}/FileMonitoring.md | 0 .../ServerOperation/Suspending/Overview.md | 53 +++++++++++++++++++ src/Private/Dump.ps1 | 3 ++ src/Private/Server.ps1 | 5 ++ 7 files changed, 74 insertions(+) rename docs/Tutorials/{ => ServerOperation}/Restarting/Overview.md (86%) rename docs/Tutorials/{Restarting/Types => ServerOperation/Restarting/Type}/AutoRestarting.md (100%) rename docs/Tutorials/{Restarting/Types => ServerOperation/Restarting/Type}/FileMonitoring.md (100%) create mode 100644 docs/Tutorials/ServerOperation/Suspending/Overview.md diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index 54bb14d49..e0724bd1b 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -64,6 +64,7 @@ Start-PodeServer -EnableBreakpoints { The steps to attach to the Pode process are as follows: 1. In a PowerShell console, start the above Pode server. You will see the following output, and you'll need the PID that is shown: + ```plain Pode v2.10.0 (PID: 28324) Listening on the following 1 endpoint(s) [1 thread(s)]: @@ -71,16 +72,19 @@ The steps to attach to the Pode process are as follows: ``` 2. In a browser or a new PowerShell console, invoke the `[GET] http://localhost:8080` Route to hit the breakpoint. + ```powershell Invoke-RestMethod -Uri 'http://localhost:8080/' ``` 3. Open another new PowerShell console, and run the following command to enter the first PowerShell console running Pode - you'll need the PID as well: + ```powershell Enter-PSHostProcess -Id '' ``` 4. Once you have entered the PowerShell console running Pode, run the below command to attach to the breakpoint: + ```powershell Get-Runspace | Where-Object { $_.Debugger.InBreakpoint } | @@ -101,6 +105,7 @@ The steps to attach to the Pode process are as follows: 7. When you are done debugging the current request, hit the `d` key. 8. When you're done with debugging altogether, you can exit the entered process as follows: + ```powershell exit ``` @@ -110,6 +115,7 @@ The steps to attach to the Pode process are as follows: If you're using [`Wait-PodeDebugger`](../../Functions/Core/Wait-PodeDebugger) then you can leave these breakpoint lines in place, and toggle them in non-developer environments by passing `-EnableBreakpoints` to [`Start-PodeServer`](../../Functions/Core/Start-PodeServer). If you don't supply `-EnableBreakpoints`, or you explicitly pass `-EnableBreakpoints:$false`, then this will disable the breakpoints from being set. You can also toggle breakpoints via the `server.psd1` [configuration file](../../Tutorials/Configuration): + ```powershell @{ Server = @{ @@ -158,6 +164,7 @@ The steps to attach to the Pode process are as follows: 1. In a PowerShell console, start the above Pode server. 2. In a browser or a new PowerShell console, invoke the `[GET] http://localhost:8080` Route to hit the breakpoint. + ```powershell Invoke-RestMethod -Uri 'http://localhost:8080/' ``` @@ -267,6 +274,7 @@ To set up default options for the memory dump feature in Pode, you can configure Enable = $true Format = 'Yaml' # Options: 'json', 'clixml', 'txt', 'bin', 'yaml' Path = './Dump' # Path to save the dump files + MaxDepth = 6 } } } @@ -276,6 +284,8 @@ To set up default options for the memory dump feature in Pode, you can configure - **Enable**: Boolean value to enable or disable the memory dump feature. - **Format**: Specifies the default format for the dump file. Supported formats are `json`, `clixml`, `txt`, `bin`, and `yaml`. - **Path**: Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. +- **MaxDepth**: Specifies the maximum depth to traverse when collecting information. + ### Overriding Default Settings at Runtime @@ -293,6 +303,7 @@ catch { ``` In this example: + - The memory dump is saved in CLIXML format instead of the default. - The dump file is saved in the specified directory (`C:\CustomDump`) instead of the default path. - The `-Halt` switch will terminate the application after the dump is saved. diff --git a/docs/Tutorials/Restarting/Overview.md b/docs/Tutorials/ServerOperation/Restarting/Overview.md similarity index 86% rename from docs/Tutorials/Restarting/Overview.md rename to docs/Tutorials/ServerOperation/Restarting/Overview.md index ba3c965b8..07ee88d21 100644 --- a/docs/Tutorials/Restarting/Overview.md +++ b/docs/Tutorials/ServerOperation/Restarting/Overview.md @@ -9,3 +9,5 @@ There are 4 ways to restart a running Pode server: 4. [`Restart-PodeServer`](../../../Functions/Core/Restart-PodeServer): This function lets you manually restart Pode from within the server. When the server restarts, it will re-invoke the `-ScriptBlock` supplied to the [`Start-PodeServer`](../../../Functions/Core/Start-PodeServer) function. This means the best approach to reload new modules/scripts it to dot-source/[`Use-PodeScript`](../../../Functions/Utilities/Use-PodeScript) your scripts into your server, as any changes to the main `scriptblock` will **not** take place. + +Certainly! Here’s an explanation on how to achieve suspending and resuming a Pode server using `Suspend-PodeServerInternal` and `Resume-PodeServerInternal`. diff --git a/docs/Tutorials/Restarting/Types/AutoRestarting.md b/docs/Tutorials/ServerOperation/Restarting/Type/AutoRestarting.md similarity index 100% rename from docs/Tutorials/Restarting/Types/AutoRestarting.md rename to docs/Tutorials/ServerOperation/Restarting/Type/AutoRestarting.md diff --git a/docs/Tutorials/Restarting/Types/FileMonitoring.md b/docs/Tutorials/ServerOperation/Restarting/Type/FileMonitoring.md similarity index 100% rename from docs/Tutorials/Restarting/Types/FileMonitoring.md rename to docs/Tutorials/ServerOperation/Restarting/Type/FileMonitoring.md diff --git a/docs/Tutorials/ServerOperation/Suspending/Overview.md b/docs/Tutorials/ServerOperation/Suspending/Overview.md new file mode 100644 index 000000000..581cf3439 --- /dev/null +++ b/docs/Tutorials/ServerOperation/Suspending/Overview.md @@ -0,0 +1,53 @@ +# Overview + +In addition to restarting, Pode provides a way to temporarily **suspend** and **resume** the server, allowing you to pause all activities and connections without completely stopping the server. This can be especially useful for debugging, troubleshooting, or performing maintenance tasks where you don’t want to fully restart the server. + +## Suspending + +To suspend a running Pode server, use the `Suspend-PodeServerInternal` function. This function will pause all active server runspaces, effectively putting the server into a suspended state. Here’s how to do it: + +1. **Run the Suspension Command**: + - Simply call `Suspend-PodeServerInternal` from within your Pode environment or script. + + ```powershell + Suspend-PodeServerInternal -Timeout 60 + ``` + + The `-Timeout` parameter specifies how long the function should wait (in seconds) for each runspace to be fully suspended. This is optional, with a default timeout of 30 seconds. + +2. **Suspension Process**: + - When you run `Suspend-PodeServerInternal`, Pode will: + - Pause all runspaces associated with the server, putting them into a debug state. + - Trigger a "Suspend" event to signify that the server is paused. + - Update the server’s status to reflect that it is now suspended. + +3. **Outcome**: + - After suspension, all server operations are halted, and the server will not respond to incoming requests until it is resumed. + +## Resuming + +Once you’ve completed any tasks or troubleshooting, you can resume the server using `Resume-PodeServerInternal`. This will restore the Pode server to its normal operational state: + +1. **Run the Resume Command**: + - Call `Resume-PodeServerInternal` to bring the server back online. + + ```powershell + Resume-PodeServerInternal + ``` + +2. **Resumption Process**: + - When `Resume-PodeServerInternal` is executed, Pode will: + - Restore all paused runspaces back to their active states. + - Trigger a "Resume" event, marking the server as active again. + - Clear the console, providing a refreshed view of the server status. + +3. **Outcome**: + - The server resumes normal operations and can now handle incoming requests again. + +## When to Use Suspend and Resume + +These functions are particularly useful when: + +- **Debugging**: If you encounter an issue, you can pause the server to inspect state or troubleshoot without a full restart. +- **Maintenance**: Suspend the server briefly during configuration changes, and then resume when ready. +- **Performance Management**: Temporarily pause during high load or for throttling purposes if required by your application logic. diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index d270de7be..2865ee296 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -21,6 +21,9 @@ .PARAMETER Path Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. +.PARAMETER MaxDepth + Specifies the maximum depth to traverse when collecting information. + .EXAMPLE try { # Simulate a critical error diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 2213c5099..94d3c28b3 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -191,9 +191,14 @@ function Show-PodeConsoleInfo { $ShowHeader ) + if ($PodeContext.Server.Quiet) { + return + } + if ($ClearHost) { Clear-Host } + if ($ShowHeader) { if ($PodeContext.Server.Suspended) { From 52574f9709e25195161361b9d8d0a42858ef9523 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 5 Nov 2024 18:10:54 -0800 Subject: [PATCH 12/34] upgrade powershell to '7.2.24' --- .github/workflows/ci-pwsh7_2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pwsh7_2.yml b/.github/workflows/ci-pwsh7_2.yml index 932bd785c..468043c36 100644 --- a/.github/workflows/ci-pwsh7_2.yml +++ b/.github/workflows/ci-pwsh7_2.yml @@ -25,7 +25,7 @@ on: env: INVOKE_BUILD_VERSION: '5.11.1' - POWERSHELL_VERSION: '7.2.19' + POWERSHELL_VERSION: '7.2.24' jobs: build: From 0bd3f8aca7492ad1d51f07b082614c2fd7d912fc Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 5 Nov 2024 18:26:56 -0800 Subject: [PATCH 13/34] Update Server.Tests.ps1 --- tests/unit/Server.Tests.ps1 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 6056b581c..8e1be9266 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -41,7 +41,7 @@ Describe 'Start-PodeInternalServer' { } It 'Calls one-off script logic' { - $PodeContext.Server = @{ Types = ([string]::Empty); Logic = {} } + $PodeContext.Server = @{ Types = ([string]::Empty); Logic = {}; Quiet = $true } Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -55,7 +55,7 @@ Describe 'Start-PodeInternalServer' { } It 'Calls smtp server logic' { - $PodeContext.Server = @{ Types = 'SMTP'; Logic = {} } + $PodeContext.Server = @{ Types = 'SMTP'; Logic = {}; Quiet = $true } Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -69,7 +69,7 @@ Describe 'Start-PodeInternalServer' { } It 'Calls tcp server logic' { - $PodeContext.Server = @{ Types = 'TCP'; Logic = {} } + $PodeContext.Server = @{ Types = 'TCP'; Logic = {}; Quiet = $true } Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -83,7 +83,7 @@ Describe 'Start-PodeInternalServer' { } It 'Calls http web server logic' { - $PodeContext.Server = @{ Types = 'HTTP'; Logic = {} } + $PodeContext.Server = @{ Types = 'HTTP'; Logic = {}; Quiet = $true } Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -203,6 +203,7 @@ Describe 'Restart-PodeInternalServer' { Storage = @{} } ScopedVariables = @{} + Quiet = $true } Metrics = @{ Server = @{ @@ -266,7 +267,7 @@ Describe 'Restart-PodeInternalServer' { } It 'Catches exception and throws it' { - Mock Write-Host { throw 'some error' } + Mock Write-PodeHost { throw 'some error' } Mock Write-PodeErrorLog {} { Restart-PodeInternalServer } | Should -Throw -ExpectedMessage 'some error' } From 6135e178518e525221310c8cc0357a8af2ddda53 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 8 Nov 2024 08:39:38 -0800 Subject: [PATCH 14/34] fixes - Add Clear-PodeKeyPressed to avoid an undeliberate sequence of pressed keys - fix OpenAPI `Show-PodeOAConsoleInfo` that shows openAPi referecente to not http protocols - Solve an issue when the first runspace to stop is not a Main one - DisableTermination disable any key input --- examples/Web-Dump.ps1 | 53 ++++++++++++++++++++++++++++++++-- src/Private/Dump.ps1 | 19 ++++++------ src/Private/Helpers.ps1 | 51 ++++++++++++++++++++++++++++---- src/Private/OpenApi.ps1 | 12 +++++--- src/Private/PodeServer.ps1 | 21 +++++++++++--- src/Private/Server.ps1 | 10 +++++-- src/Public/Core.ps1 | 59 ++++++++++++++++++++++---------------- 7 files changed, 174 insertions(+), 51 deletions(-) diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 index 2ecaa9e5d..9ce2c258a 100644 --- a/examples/Web-Dump.ps1 +++ b/examples/Web-Dump.ps1 @@ -39,13 +39,20 @@ try { catch { throw } # Start Pode server with specified script block -Start-PodeServer -Threads 1 -ScriptBlock { +Start-PodeServer -Threads 4 -ScriptBlock { # listen on localhost:8081 Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8083 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8025 -Protocol Smtp + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -Name 'WS1' + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'WS' + Add-PodeEndpoint -Address localhost -Port 8100 -Protocol Tcp + # set view engine to pode renderer - Set-PodeViewEngine -Type Pode + Set-PodeViewEngine -Type Html # Enable error logging New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging @@ -88,4 +95,46 @@ Start-PodeServer -Threads 1 -ScriptBlock { } | Set-PodeOARouteInfo -Summary 'Dump state' -Description 'Dump the memory state of the server.' -Tags 'dump' -OperationId 'dump'-PassThru | Set-PodeOARequest -Parameters (New-PodeOAStringProperty -Name 'format' -Description 'Dump export format.' -Enum 'json', 'clixml', 'txt', 'bin', 'yaml' -Default 'json' | ConvertTo-PodeOAParameter -In Query ) } + + Add-PodeVerb -Verb 'HELLO' -ScriptBlock { + Write-PodeTcpClient -Message 'HI' + 'here' | Out-Default + } + + # setup an smtp handler + Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { + Write-PodeHost '- - - - - - - - - - - - - - - - - -' + Write-PodeHost $SmtpEvent.Email.From + Write-PodeHost $SmtpEvent.Email.To + Write-PodeHost '|' + Write-PodeHost $SmtpEvent.Email.Body + Write-PodeHost '|' + # Write-PodeHost $SmtpEvent.Email.Data + # Write-PodeHost '|' + $SmtpEvent.Email.Attachments | Out-Default + if ($SmtpEvent.Email.Attachments.Length -gt 0) { + #$SmtpEvent.Email.Attachments[0].Save('C:\temp') + } + Write-PodeHost '|' + $SmtpEvent.Email | Out-Default + $SmtpEvent.Request | out-default + $SmtpEvent.Email.Headers | out-default + Write-PodeHost '- - - - - - - - - - - - - - - - - -' + } + + # GET request for web page + Add-PodeRoute -Method Get -Path '/' -EndpointName 'WS' -ScriptBlock { + Write-PodeViewResponse -Path 'websockets' + } + + # SIGNAL route, to return current date + Add-PodeSignalRoute -Path '/' -ScriptBlock { + $msg = $SignalEvent.Data.Message + + if ($msg -ieq '[date]') { + $msg = [datetime]::Now.ToString() + } + + Send-PodeSignal -Value @{ message = $msg } + } } \ No newline at end of file diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 2865ee296..67a5f409c 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -170,16 +170,19 @@ function Invoke-PodeDumpInternal { # Check if RunspacePools is not null before iterating $runspacePoolDetails = @() - $runspaces = Get-Runspace -name 'Pode_*' + + # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' } | Sort-Object Name + $runspaceDetails = @{} foreach ($r in $runspaces) { - $runspaceDetails[$r.Name] = @{ - Id = $r.Id - Name = $r.Name - InitialSessionState = $r.InitialSessionState - RunspaceStateInfo = $r.RunspaceStateInfo - } - $runspaceDetails[$r.Name].ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r + $runspaceDetails[$r.Name] = @{ + Id = $r.Id + Name = $r.Name + InitialSessionState = $r.InitialSessionState + RunspaceStateInfo = $r.RunspaceStateInfo + } + $runspaceDetails[$r.Name].ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r } if ($null -ne $PodeContext.RunspacePools) { diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 629b40ab7..6d3f7d6d6 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -579,6 +579,10 @@ function Test-PodeRestartPressed { $Key = $null ) + if ($PodeContext.Server.DisableTermination) { + return $false + } + return (Test-PodeKeyPressed -Key $Key -Character 'r') } @@ -588,6 +592,10 @@ function Test-PodeOpenBrowserPressed { $Key = $null ) + if ($PodeContext.Server.DisableTermination) { + return $false + } + return (Test-PodeKeyPressed -Key $Key -Character 'b') } @@ -597,6 +605,9 @@ function Test-PodeDumpPressed { $Key = $null ) + if ($PodeContext.Server.DisableTermination) { + return $false + } return (Test-PodeKeyPressed -Key $Key -Character 'd') } @@ -606,19 +617,47 @@ function Test-PodeSuspendPressed { $Key = $null ) + if ($PodeContext.Server.DisableTermination) { + return $false + } + return (Test-PodeKeyPressed -Key $Key -Character 'u') } -function Test-PodeResumePressed { - param( - [Parameter()] - $Key = $null - ) - return (Test-PodeKeyPressed -Key $Key -Character 'a') +<# +.SYNOPSIS + Clears any remaining keys in the console input buffer. + +.DESCRIPTION + The `Clear-PodeKeyPressed` function checks if there are any keys remaining in the input buffer + and discards them, ensuring that no leftover key presses interfere with subsequent reads. + +.EXAMPLE + Clear-PodeKeyPressed + [Console]::ReadKey($true) + + This example clears the buffer and then reads a new key without interference. + +.NOTES + This function is useful when using `[Console]::ReadKey($true)` to prevent previous key presses + from affecting the input. + +#> +function Clear-PodeKeyPressed { + if ($PodeContext.Server.DisableTermination) { + return $false + } + + # Clear any remaining keys in the input buffer + while ([Console]::KeyAvailable) { + + [Console]::ReadKey($true) | Out-Null + } } + function Test-PodeKeyPressed { param( [Parameter()] diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index 493a12f79..f948a140d 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -2422,13 +2422,17 @@ function Show-PodeOAConsoleInfo { # Specification Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow $PodeContext.Server.EndpointsInfo | ForEach-Object { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) - Write-PodeHost " . $url" -ForegroundColor White + if ($_.Pool -eq 'web') { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) + Write-PodeHost " . $url" -ForegroundColor White + } } Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow $PodeContext.Server.EndpointsInfo | ForEach-Object { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) - Write-PodeHost " . $url" -ForegroundColor White + if ($_.Pool -eq 'web') { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) + Write-PodeHost " . $url" -ForegroundColor White + } } } } diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index c954631e5..34e830f0d 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -68,8 +68,8 @@ function Start-PodeWebServer { } } - # create the listener - $listener = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)Listener -CancellationToken `$PodeContext.Tokens.Cancellation.Token"))) + # Create the listener + $listener = & $("New-Pode$($PodeContext.Server.ListenerType)Listener") -CancellationToken $PodeContext.Tokens.Cancellation.Token $listener.ErrorLoggingEnabled = (Test-PodeErrorLoggingEnabled) $listener.ErrorLoggingLevels = @(Get-PodeErrorLoggingLevel) $listener.RequestTimeout = $PodeContext.Server.Request.Timeout @@ -79,7 +79,20 @@ function Start-PodeWebServer { try { # register endpoints on the listener $endpoints | ForEach-Object { - $socket = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)ListenerSocket -Name `$_.Name -Address `$_.Address -Port `$_.Port -SslProtocols `$_.SslProtocols -Type `$endpointsMap[`$_.Key].Type -Certificate `$_.Certificate -AllowClientCertificate `$_.AllowClientCertificate -DualMode:`$_.DualMode"))) + # Create a hashtable of parameters for splatting + $socketParams = @{ + Name = $_.Name + Address = $_.Address + Port = $_.Port + SslProtocols = $_.SslProtocols + Type = $endpointsMap[$_.Key].Type + Certificate = $_.Certificate + AllowClientCertificate = $_.AllowClientCertificate + DualMode = $_.DualMode + } + + # Initialize a new listener socket with splatting + $socket = & $("New-Pode$($PodeContext.Server.ListenerType)ListenerSocket") @socketParams $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout if (!$_.IsIPAddress) { @@ -280,7 +293,7 @@ function Start-PodeWebServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Web -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Web -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } } diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 94d3c28b3..cc07fb6bf 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -198,7 +198,7 @@ function Show-PodeConsoleInfo { if ($ClearHost) { Clear-Host } - + if ($ShowHeader) { if ($PodeContext.Server.Suspended) { @@ -365,6 +365,9 @@ function Restart-PodeInternalServer { # restart the server $PodeContext.Metrics.Server.RestartCount++ + + # Update the server's suspended state + $PodeContext.Server.Suspended = $false Start-PodeInternalServer } catch { @@ -423,8 +426,9 @@ function Suspend-PodeServerInternal { # Update the server's suspended state $PodeContext.Server.Suspended = $true - # Retrieve all runspaces related to Pode - $runspaces = Get-Runspace -name 'Pode_*' + # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' } | Sort-Object Name + foreach ($runspace in $runspaces) { try { # Attach debugger to the runspace diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 3397b3019..621579e73 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -206,38 +206,49 @@ function Start-PodeServer { } # sit here waiting for termination/cancellation, or to restart the server - while (!(Test-PodeTerminationPressed -Key $key) -and !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) { - Start-Sleep -Seconds 1 + while ( !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) { + try { + Start-Sleep -Seconds 1 - # get the next key presses - $key = Get-PodeConsoleKey + if (!$PodeContext.Server.DisableTermination) { + # get the next key presses + $key = Get-PodeConsoleKey + } - # check for internal restart - if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { - Restart-PodeInternalServer - } + # check for internal restart + if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { + Restart-PodeInternalServer + } - if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { - Invoke-PodeDumpInternal - if ($PodeContext.Server.Debug.Dump.Param.Halt) { - Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' - break + if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { + Invoke-PodeDumpInternal + if ($PodeContext.Server.Debug.Dump.Param.Halt) { + Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' + break + } } - } - if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { - if ( $PodeContext.Server.Suspended) { - Resume-PodeServerInternal + if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { + if ( $PodeContext.Server.Suspended) { + Resume-PodeServerInternal + } + else { + Suspend-PodeServerInternal + } } - else { - Suspend-PodeServerInternal + + # check for open browser + if (Test-PodeOpenBrowserPressed -Key $key) { + Invoke-PodeEvent -Type Browser + Start-Process (Get-PodeEndpointUrl) } - } - # check for open browser - if (Test-PodeOpenBrowserPressed -Key $key) { - Invoke-PodeEvent -Type Browser - Start-Process (Get-PodeEndpointUrl) + if (Test-PodeTerminationPressed -Key $key) { + break + } + } + finally { + Clear-PodeKeyPressed } } From 82a60208deaf214833425774dbdb16fa60de11d9 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 10 Nov 2024 20:41:53 -0800 Subject: [PATCH 15/34] Fix CTRL B issue --- src/Private/Helpers.ps1 | 6 +++++- src/Private/Server.ps1 | 3 +++ src/Public/Core.ps1 | 7 +++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 6d3f7d6d6..59d91022c 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -650,7 +650,7 @@ function Clear-PodeKeyPressed { return $false } - # Clear any remaining keys in the input buffer + # Clear any remaining keys in the input buffer while ([Console]::KeyAvailable) { [Console]::ReadKey($true) | Out-Null @@ -2667,6 +2667,10 @@ function Get-PodeEndpointUrl { } } + if ($null -eq $Endpoint) { + return $null + } + $url = $Endpoint.Url if ([string]::IsNullOrWhiteSpace($url)) { $url = "$($Endpoint.Protocol)://$($Endpoint.FriendlyName):$($Endpoint.Port)" diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index cc07fb6bf..95e62bab3 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -225,6 +225,9 @@ function Show-PodeConsoleInfo { Write-PodeHost " Ctrl+C : $($Podelocale.GracefullyTerminateMessage)" -ForegroundColor Cyan Write-PodeHost " Ctrl+R : $($Podelocale.RestartServerMessage)" -ForegroundColor Cyan Write-PodeHost " Ctrl+U : $resumeOrSuspend" -ForegroundColor Cyan + if (Get-PodeEndpointUrl) { + Write-PodeHost ' Ctrl+B : Browser' -ForegroundColor Cyan + } if ($PodeContext.Server.Debug.Dump.Enabled) { Write-PodeHost " Ctrl+D : $($Podelocale.GenerateDiagnosticDumpMessage)" -ForegroundColor Cyan diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 621579e73..51dfbacfd 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -239,8 +239,11 @@ function Start-PodeServer { # check for open browser if (Test-PodeOpenBrowserPressed -Key $key) { - Invoke-PodeEvent -Type Browser - Start-Process (Get-PodeEndpointUrl) + $url = Get-PodeEndpointUrl + if ($null -ne $url) { + Invoke-PodeEvent -Type Browser + Start-Process $url + } } if (Test-PodeTerminationPressed -Key $key) { From 49f962bf10adc93f3a3061de5548be65124c36e3 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 13 Nov 2024 11:15:39 -0800 Subject: [PATCH 16/34] fixes --- docs/release-notes.md | 2 +- src/Locales/ar/Pode.psd1 | 1 + src/Locales/de/Pode.psd1 | 1 + src/Locales/en-us/Pode.psd1 | 1 + src/Locales/en/Pode.psd1 | 1 + src/Locales/es/Pode.psd1 | 1 + src/Locales/fr/Pode.psd1 | 1 + src/Locales/it/Pode.psd1 | 1 + src/Locales/ja/Pode.psd1 | 1 + src/Locales/ko/Pode.psd1 | 1 + src/Locales/nl/Pode.psd1 | 1 + src/Locales/pl/Pode.psd1 | 1 + src/Locales/pt/Pode.psd1 | 1 + src/Locales/zh/Pode.psd1 | 1 + src/Private/Server.ps1 | 5 +-- src/Public/Core.ps1 | 70 ++++++++++++++++++------------------- 16 files changed, 52 insertions(+), 38 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 674f0b5e7..192044d07 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -732,7 +732,7 @@ Date: 24th May 2020 ### Enhancements * #533: Support on states for inclusion/exlcusions when saving, and scopes on states * #538: Support to batch log items together before sending them off to be recorded -* #540: Adds a Ctrl+B shortcutto open the server in the default browser +* #540: Adds a Ctrl+B shortcut to open the server in the default browser * #542: Add new switch to help toggling of Status Page exception message * #548: Adds new `Get-PodeSchedule` and `Get-PodeTimer` functions * #549: Support for calculating a schedule's next trigger datetime diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 3bc5c3a52..c84b5aef1 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'جارٍ إعادة التشغيل' suspendedMessage = 'معلق' runningMessage = 'يعمل' + openHttpEndpointMessage = 'افتح أول نقطة نهاية HTTP في المتصفح الافتراضي.' } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index aa62fe4d3..1f99d815d 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Neustart' suspendedMessage = 'Angehalten' runningMessage = 'Läuft' + openHttpEndpointMessage = 'Öffnen Sie den ersten HTTP-Endpunkt im Standardbrowser.' } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index cfd715ac4..7c8dff863 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Restarting' suspendedMessage = 'Suspended' runningMessage = 'Running' + openHttpEndpointMessage = 'Open the first HTTP endpoint in the default browser.' } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 11a927c14..2b09c8f7c 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Restarting' suspendedMessage = 'Suspended' runningMessage = 'Running' + openHttpEndpointMessage = 'Open the first HTTP endpoint in the default browser.' } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 06402b4ca..d10750bba 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Reiniciando' suspendedMessage = 'Suspendido' runningMessage = 'En ejecución' + openHttpEndpointMessage = 'Abrir el primer endpoint HTTP en el navegador predeterminado.' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 1509974e9..d910bdc74 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Redémarrage' suspendedMessage = 'Suspendu' runningMessage = "En cours d'exécution" + openHttpEndpointMessage = 'Ouvrez le premier point de terminaison HTTP dans le navigateur par défaut.' } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 7ce844e66..53b29fb34 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Riavvio' suspendedMessage = 'Sospeso' runningMessage = 'In esecuzione' + openHttpEndpointMessage = 'Apri il primo endpoint HTTP nel browser predefinito.' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 922beb8e3..8e6cb3178 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = '再起動中' suspendedMessage = '一時停止中' runningMessage = '実行中' + openHttpEndpointMessage = 'デフォルトのブラウザで最初の HTTP エンドポイントを開きます。' } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 1e489607e..6960afe2e 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = '재시작 중' suspendedMessage = '일시 중지됨' runningMessage = '실행 중' + openHttpEndpointMessage = '기본 브라우저에서 첫 번째 HTTP 엔드포인트를 엽니다.' } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 1b07fbccd..2d359cc5f 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Herstarten' suspendedMessage = 'Gepauzeerd' runningMessage = 'Actief' + openHttpEndpointMessage = 'Open het eerste HTTP-eindpunt in de standaardbrowser.' } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index 2b295321f..f003d5036 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Ponowne uruchamianie' suspendedMessage = 'Wstrzymany' runningMessage = 'Działa' + openHttpEndpointMessage = 'Otwórz pierwszy punkt końcowy HTTP w domyślnej przeglądarce.' } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index 9b2412f5c..de9a4340c 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = 'Reiniciando' suspendedMessage = 'Suspenso' runningMessage = 'Executando' + openHttpEndpointMessage = 'Abrir o primeiro endpoint HTTP no navegador padrão.' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index cf708346c..46b3cf791 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -304,4 +304,5 @@ restartingMessage = '正在重启' suspendedMessage = '已暂停' runningMessage = '运行中' + openHttpEndpointMessage = '在默认浏览器中打开第一个 HTTP 端点。' } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 95e62bab3..c93cd77fa 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -225,8 +225,9 @@ function Show-PodeConsoleInfo { Write-PodeHost " Ctrl+C : $($Podelocale.GracefullyTerminateMessage)" -ForegroundColor Cyan Write-PodeHost " Ctrl+R : $($Podelocale.RestartServerMessage)" -ForegroundColor Cyan Write-PodeHost " Ctrl+U : $resumeOrSuspend" -ForegroundColor Cyan - if (Get-PodeEndpointUrl) { - Write-PodeHost ' Ctrl+B : Browser' -ForegroundColor Cyan + + if ((Get-PodeEndpointUrl) -and !($PodeContext.Server.Suspended)) { + Write-PodeHost " Ctrl+B : $($Podelocale.OpenHttpEndpointMessage)" -ForegroundColor Cyan } if ($PodeContext.Server.Debug.Dump.Enabled) { diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 51dfbacfd..cb4aa0ee7 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -207,51 +207,50 @@ function Start-PodeServer { # sit here waiting for termination/cancellation, or to restart the server while ( !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) { - try { - Start-Sleep -Seconds 1 + Start-Sleep -Seconds 1 - if (!$PodeContext.Server.DisableTermination) { - # get the next key presses - $key = Get-PodeConsoleKey - } + if (!$PodeContext.Server.DisableTermination) { + # get the next key presses + $key = Get-PodeConsoleKey + } - # check for internal restart - if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { - Restart-PodeInternalServer - } + # check for internal restart + if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { + Clear-PodeKeyPressed + Restart-PodeInternalServer + } - if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { - Invoke-PodeDumpInternal - if ($PodeContext.Server.Debug.Dump.Param.Halt) { - Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' - break - } + if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { + Clear-PodeKeyPressed + Invoke-PodeDumpInternal + if ($PodeContext.Server.Debug.Dump.Param.Halt) { + Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' + break } + } - if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { - if ( $PodeContext.Server.Suspended) { - Resume-PodeServerInternal - } - else { - Suspend-PodeServerInternal - } + if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { + Clear-PodeKeyPressed + if ( $PodeContext.Server.Suspended) { + Resume-PodeServerInternal } - - # check for open browser - if (Test-PodeOpenBrowserPressed -Key $key) { - $url = Get-PodeEndpointUrl - if ($null -ne $url) { - Invoke-PodeEvent -Type Browser - Start-Process $url - } + else { + Suspend-PodeServerInternal } + } - if (Test-PodeTerminationPressed -Key $key) { - break + # check for open browser + if (Test-PodeOpenBrowserPressed -Key $key) { + Clear-PodeKeyPressed + $url = Get-PodeEndpointUrl + if ($null -ne $url) { + Invoke-PodeEvent -Type Browser + Start-Process $url } } - finally { - Clear-PodeKeyPressed + + if (Test-PodeTerminationPressed -Key $key) { + break } } @@ -260,6 +259,7 @@ function Start-PodeServer { Write-PodeHost $PodeLocale.iisShutdownMessage -NoNewLine -ForegroundColor Yellow Write-PodeHost ' ' -NoNewLine } + # Terminating... Write-PodeHost $PodeLocale.terminatingMessage -NoNewLine -ForegroundColor Yellow Invoke-PodeEvent -Type Terminate From 82c2a2802bd671fc49fde21ea7390b398f08aaea Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 22 Nov 2024 06:10:39 -0800 Subject: [PATCH 17/34] add net9 --- pode.build.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/pode.build.ps1 b/pode.build.ps1 index d82731d5c..13bdd088c 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -113,6 +113,7 @@ function Invoke-PodeBuildDotnetBuild($target) { # Determine if the target framework is compatible $isCompatible = $False switch ($majorVersion) { + 9 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0','net9.0')) { $isCompatible = $True } } 8 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0')) { $isCompatible = $True } } 7 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } 6 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } From f0c8b45b31517fb8ba42f15b449d79b3f7df4fc9 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 24 Nov 2024 17:05:17 -0800 Subject: [PATCH 18/34] doc fixes --- docs/Getting-Started/Debug.md | 7 +++---- .../ServerOperation/Restarting/Overview.md | 2 +- .../ServerOperation/Suspending/Overview.md | 16 ++++++++-------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index e0724bd1b..d081219f3 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -254,7 +254,6 @@ Add-PodeSchedule -Name 'TestSchedule' -Cron '@hourly' -ScriptBlock { In this example, the schedule outputs the name of the runspace executing the script block every hour. This can be useful for logging and monitoring purposes when dealing with multiple schedules or tasks. - ## Memory Dump for Diagnostics Pode provides a powerful memory dump feature to capture detailed information during critical failures or fatal exceptions. This feature, triggered by the `Invoke-PodeDump` function, captures the state of your application, including memory usage, runspace details, variables, and stack traces. You can configure the dump format, enable or disable it, and specify the save location. By default, Pode saves the dump in JSON format, but you can also choose from other supported formats. @@ -308,11 +307,11 @@ In this example: - The dump file is saved in the specified directory (`C:\CustomDump`) instead of the default path. - The `-Halt` switch will terminate the application after the dump is saved. -### Using the Dump Feature in Pode +### Using the Dump Feature To use the dump feature effectively in your Pode server, you may want to include it in specific places within your code to capture state information when critical errors occur. -Example usage in a Pode server: +Example : ```powershell Start-PodeServer -EnableBreakpoints { @@ -343,4 +342,4 @@ In this setup, if an error occurs in the route, `Invoke-PodeDump` is called, cap - **Binary** is compact and suitable for raw state captures but requires deserialization for inspection. - **Setting the Path**: Use a dedicated folder for dump files (e.g., `./Dump`) to keep diagnostic files organized. The default path in the configuration can be overridden at runtime if needed. -With these configurations and usage practices, the memory dump feature in Pode can provide a powerful tool for diagnostics and debugging, capturing critical state information at the time of failure. \ No newline at end of file +With these configurations and usage practices, the memory dump feature in Pode can provide a powerful tool for diagnostics and debugging, capturing critical state information at the time of failure. diff --git a/docs/Tutorials/ServerOperation/Restarting/Overview.md b/docs/Tutorials/ServerOperation/Restarting/Overview.md index 07ee88d21..eae70abb5 100644 --- a/docs/Tutorials/ServerOperation/Restarting/Overview.md +++ b/docs/Tutorials/ServerOperation/Restarting/Overview.md @@ -10,4 +10,4 @@ There are 4 ways to restart a running Pode server: When the server restarts, it will re-invoke the `-ScriptBlock` supplied to the [`Start-PodeServer`](../../../Functions/Core/Start-PodeServer) function. This means the best approach to reload new modules/scripts it to dot-source/[`Use-PodeScript`](../../../Functions/Utilities/Use-PodeScript) your scripts into your server, as any changes to the main `scriptblock` will **not** take place. -Certainly! Here’s an explanation on how to achieve suspending and resuming a Pode server using `Suspend-PodeServerInternal` and `Resume-PodeServerInternal`. +Certainly! Here’s an explanation on how to achieve suspending and resuming a Pode server using [`Suspend-PodeServer`](../../../Functions/Core/Resume-PodeServer) and [`Resume-PodeServer`](../../../Functions/Core/Suspend-PodeServer). diff --git a/docs/Tutorials/ServerOperation/Suspending/Overview.md b/docs/Tutorials/ServerOperation/Suspending/Overview.md index 581cf3439..dac129abb 100644 --- a/docs/Tutorials/ServerOperation/Suspending/Overview.md +++ b/docs/Tutorials/ServerOperation/Suspending/Overview.md @@ -4,19 +4,19 @@ In addition to restarting, Pode provides a way to temporarily **suspend** and ** ## Suspending -To suspend a running Pode server, use the `Suspend-PodeServerInternal` function. This function will pause all active server runspaces, effectively putting the server into a suspended state. Here’s how to do it: +To suspend a running Pode server, use the `Suspend-PodeServer` function. This function will pause all active server runspaces, effectively putting the server into a suspended state. Here’s how to do it: 1. **Run the Suspension Command**: - - Simply call `Suspend-PodeServerInternal` from within your Pode environment or script. + - Simply call `Suspend-PodeServer` from within your Pode environment or script. ```powershell - Suspend-PodeServerInternal -Timeout 60 + Suspend-PodeServer -Timeout 60 ``` The `-Timeout` parameter specifies how long the function should wait (in seconds) for each runspace to be fully suspended. This is optional, with a default timeout of 30 seconds. 2. **Suspension Process**: - - When you run `Suspend-PodeServerInternal`, Pode will: + - When you run `Suspend-PodeServer`, Pode will: - Pause all runspaces associated with the server, putting them into a debug state. - Trigger a "Suspend" event to signify that the server is paused. - Update the server’s status to reflect that it is now suspended. @@ -26,17 +26,17 @@ To suspend a running Pode server, use the `Suspend-PodeServerInternal` function. ## Resuming -Once you’ve completed any tasks or troubleshooting, you can resume the server using `Resume-PodeServerInternal`. This will restore the Pode server to its normal operational state: +Once you’ve completed any tasks or troubleshooting, you can resume the server using `Resume-PodeServer`. This will restore the Pode server to its normal operational state: 1. **Run the Resume Command**: - - Call `Resume-PodeServerInternal` to bring the server back online. + - Call `Resume-PodeServer` to bring the server back online. ```powershell - Resume-PodeServerInternal + Resume-PodeServer ``` 2. **Resumption Process**: - - When `Resume-PodeServerInternal` is executed, Pode will: + - When `Resume-PodeServer` is executed, Pode will: - Restore all paused runspaces back to their active states. - Trigger a "Resume" event, marking the server as active again. - Clear the console, providing a refreshed view of the server status. From dc76df386bd2868827b6d6b838f0bdac4d4e865f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 30 Nov 2024 11:48:56 -0800 Subject: [PATCH 19/34] first review --- docs/Getting-Started/Debug.md | 7 +- .../ServerOperation/Restarting/Overview.md | 2 - src/Embedded/DebuggerHandler.cs | 109 +++++++++ src/Embedded/NewDebuggerHandler.cs | 97 ++++++++ src/Embedded/OldDebuggerHandler.cs | 109 +++++++++ src/Private/Context.ps1 | 20 +- src/Private/Dump.ps1 | 218 +++++++----------- src/Private/Endpoints.ps1 | 40 ++-- src/Private/FileMonitor.ps1 | 2 +- src/Private/Helpers.ps1 | 88 ++++++- src/Private/OpenApi.ps1 | 85 ++++--- src/Private/Server.ps1 | 161 +++++++++---- src/Public/Core.ps1 | 157 ++++++++----- src/Public/Utilities.ps1 | 14 +- tests/unit/Server.Tests.ps1 | 14 +- 15 files changed, 801 insertions(+), 322 deletions(-) create mode 100644 src/Embedded/DebuggerHandler.cs create mode 100644 src/Embedded/NewDebuggerHandler.cs create mode 100644 src/Embedded/OldDebuggerHandler.cs diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index d081219f3..f53098f8e 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -182,8 +182,6 @@ The steps to attach to the Pode process are as follows: 6. When you are done debugging the current request, hit the `d` key. - - ## Managing Runspace Names ### Internal Runspace Naming @@ -266,9 +264,6 @@ To set up default options for the memory dump feature in Pode, you can configure @{ Server = @{ Debug = @{ - Breakpoints = @{ - Enable = $true - } Dump = @{ Enable = $true Format = 'Yaml' # Options: 'json', 'clixml', 'txt', 'bin', 'yaml' @@ -285,7 +280,6 @@ To set up default options for the memory dump feature in Pode, you can configure - **Path**: Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. - **MaxDepth**: Specifies the maximum depth to traverse when collecting information. - ### Overriding Default Settings at Runtime The `Invoke-PodeDump` function allows you to override these defaults at runtime by passing parameters to specify the format and path. This can be useful for debugging in specific cases without altering the default configuration. @@ -343,3 +337,4 @@ In this setup, if an error occurs in the route, `Invoke-PodeDump` is called, cap - **Setting the Path**: Use a dedicated folder for dump files (e.g., `./Dump`) to keep diagnostic files organized. The default path in the configuration can be overridden at runtime if needed. With these configurations and usage practices, the memory dump feature in Pode can provide a powerful tool for diagnostics and debugging, capturing critical state information at the time of failure. + \ No newline at end of file diff --git a/docs/Tutorials/ServerOperation/Restarting/Overview.md b/docs/Tutorials/ServerOperation/Restarting/Overview.md index eae70abb5..ba3c965b8 100644 --- a/docs/Tutorials/ServerOperation/Restarting/Overview.md +++ b/docs/Tutorials/ServerOperation/Restarting/Overview.md @@ -9,5 +9,3 @@ There are 4 ways to restart a running Pode server: 4. [`Restart-PodeServer`](../../../Functions/Core/Restart-PodeServer): This function lets you manually restart Pode from within the server. When the server restarts, it will re-invoke the `-ScriptBlock` supplied to the [`Start-PodeServer`](../../../Functions/Core/Start-PodeServer) function. This means the best approach to reload new modules/scripts it to dot-source/[`Use-PodeScript`](../../../Functions/Utilities/Use-PodeScript) your scripts into your server, as any changes to the main `scriptblock` will **not** take place. - -Certainly! Here’s an explanation on how to achieve suspending and resuming a Pode server using [`Suspend-PodeServer`](../../../Functions/Core/Resume-PodeServer) and [`Resume-PodeServer`](../../../Functions/Core/Suspend-PodeServer). diff --git a/src/Embedded/DebuggerHandler.cs b/src/Embedded/DebuggerHandler.cs new file mode 100644 index 000000000..79aa4cded --- /dev/null +++ b/src/Embedded/DebuggerHandler.cs @@ -0,0 +1,109 @@ +using System; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Collections.ObjectModel; + +namespace Pode.Embedded +{ + public class DebuggerHandler + { + // Collection to store variables collected during the debugging session + private static PSDataCollection variables = new PSDataCollection(); + + // Event handler for the DebuggerStop event + private static EventHandler debuggerStopHandler; + + // Flag to indicate whether the DebuggerStop event has been triggered + private static bool eventTriggered = false; + + // Flag to control whether variables should be collected during the DebuggerStop event + private static bool shouldCollectVariables = true; + + // Method to attach the DebuggerStop event handler to the runspace's debugger + public static void AttachDebugger(Runspace runspace, bool collectVariables = true) + { + // Set the collection flag based on the parameter + shouldCollectVariables = collectVariables; + + // Initialize the event handler with the OnDebuggerStop method + debuggerStopHandler = new EventHandler(OnDebuggerStop); + + // Attach the event handler to the DebuggerStop event of the runspace's debugger + runspace.Debugger.DebuggerStop += debuggerStopHandler; + } + + // Method to detach the DebuggerStop event handler from the runspace's debugger + public static void DetachDebugger(Runspace runspace) + { + if (debuggerStopHandler != null) + { + // Remove the event handler to prevent further event handling + runspace.Debugger.DebuggerStop -= debuggerStopHandler; + + // Set the handler to null to clean up + debuggerStopHandler = null; + } + } + + // Event handler method that gets called when the debugger stops + private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) + { + // Set the eventTriggered flag to true + eventTriggered = true; + + // Cast the sender to a Debugger object + var debugger = sender as Debugger; + if (debugger != null) + { + // Enable step mode to allow for command execution during the debug stop + debugger.SetDebuggerStepMode(true); + + PSCommand command = new PSCommand(); + + if (shouldCollectVariables) + { + // Collect variables + command.AddCommand("Get-PodeDumpScopedVariable"); + } + else + { + // Execute a break + command.AddCommand( "while( $PodeContext.Server.Suspended){ Start-sleep 1}"); + } + + // Create a collection to store the command output + PSDataCollection outputCollection = new PSDataCollection(); + + // Execute the command within the debugger + debugger.ProcessCommand(command, outputCollection); + + // Add results to the variables collection if collecting variables + if (shouldCollectVariables) + { + foreach (var output in outputCollection) + { + variables.Add(output); + } + } + else + { + // Ensure the debugger remains ready for further interaction + debugger.SetDebuggerStepMode(true); + } + } + } + + + // Method to check if the DebuggerStop event has been triggered + public static bool IsEventTriggered() + { + return eventTriggered; + } + + // Method to retrieve the collected variables + public static PSDataCollection GetVariables() + { + return variables; + } + } +} \ No newline at end of file diff --git a/src/Embedded/NewDebuggerHandler.cs b/src/Embedded/NewDebuggerHandler.cs new file mode 100644 index 000000000..422481161 --- /dev/null +++ b/src/Embedded/NewDebuggerHandler.cs @@ -0,0 +1,97 @@ +using System; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Collections.ObjectModel; + +namespace Pode.Embedded +{ + public class DebuggerHandler : IDisposable + { + // Collection to store variables collected during the debugging session + public PSDataCollection Variables { get; private set; } = new PSDataCollection(); + + // Event handler for the DebuggerStop event + private EventHandler DebuggerStopHandler; + + // Flag to indicate whether the DebuggerStop event has been triggered + public bool IsEventTriggered { get; private set; } = false; + + // Flag to control whether variables should be collected during the DebuggerStop event + private bool CollectVariables = true; + + // Runspace object to store the runspace that the debugger is attached to + private Runspace Runspace; + + public DebuggerHandler(Runspace runspace, bool collectVariables = true) + { + // Set the collection flag and Runspace object + CollectVariables = collectVariables; + Runspace = runspace; + + // Initialize the event handler with the OnDebuggerStop method + DebuggerStopHandler = new EventHandler(OnDebuggerStop); + Runspace.Debugger.DebuggerStop += DebuggerStopHandler; + } + + // Method to detach the DebuggerStop event handler from the runspace's debugger, and general clean-up + public void Dispose() + { + IsEventTriggered = false; + + // Remove the event handler to prevent further event handling + if (DebuggerStopHandler != default(EventHandler)) + { + Runspace.Debugger.DebuggerStop -= DebuggerStopHandler; + DebuggerStopHandler = null; + } + + // Clean-up variables + Runspace = default(Runspace); + Variables.Clear(); + + // Garbage collection + GC.SuppressFinalize(this); + } + + // Event handler method that gets called when the debugger stops + private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) + { + // Set the eventTriggered flag to true + IsEventTriggered = true; + + // Cast the sender to a Debugger object + var debugger = sender as Debugger; + if (debugger == default(Debugger)) + { + return; + } + + // Enable step mode to allow for command execution during the debug stop + debugger.SetDebuggerStepMode(true); + + // Collect variables or hang the debugger + var command = new PSCommand(); + command.AddCommand(CollectVariables + ? "Get-PodeDumpScopedVariable" + : "while($PodeContext.Server.Suspended) { Start-Sleep -Milliseconds 500 }"); + + // Execute the command within the debugger + var outputCollection = new PSDataCollection(); + debugger.ProcessCommand(command, outputCollection); + + // Add results to the variables collection if collecting variables + if (CollectVariables) + { + foreach (var output in outputCollection) + { + Variables.Add(output); + } + } + else + { + // Ensure the debugger remains ready for further interaction + debugger.SetDebuggerStepMode(true); + } + } + } +} \ No newline at end of file diff --git a/src/Embedded/OldDebuggerHandler.cs b/src/Embedded/OldDebuggerHandler.cs new file mode 100644 index 000000000..5db73784b --- /dev/null +++ b/src/Embedded/OldDebuggerHandler.cs @@ -0,0 +1,109 @@ +using System; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Collections.ObjectModel; + +namespace Pode.Embedded +{ + public static class DebuggerHandler + { + // Collection to store variables collected during the debugging session + private static PSDataCollection variables = new PSDataCollection(); + + // Event handler for the DebuggerStop event + private static EventHandler debuggerStopHandler; + + // Flag to indicate whether the DebuggerStop event has been triggered + private static bool eventTriggered = false; + + // Flag to control whether variables should be collected during the DebuggerStop event + private static bool shouldCollectVariables = true; + + // Method to attach the DebuggerStop event handler to the runspace's debugger + public static void AttachDebugger(Runspace runspace, bool collectVariables = true) + { + // Set the collection flag based on the parameter + shouldCollectVariables = collectVariables; + + // Initialize the event handler with the OnDebuggerStop method + debuggerStopHandler = new EventHandler(OnDebuggerStop); + + // Attach the event handler to the DebuggerStop event of the runspace's debugger + runspace.Debugger.DebuggerStop += debuggerStopHandler; + } + + // Method to detach the DebuggerStop event handler from the runspace's debugger + public static void DetachDebugger(Runspace runspace) + { + if (debuggerStopHandler != null) + { + // Remove the event handler to prevent further event handling + runspace.Debugger.DebuggerStop -= debuggerStopHandler; + + // Set the handler to null to clean up + debuggerStopHandler = null; + } + } + + // Event handler method that gets called when the debugger stops + private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) + { + // Set the eventTriggered flag to true + eventTriggered = true; + + // Cast the sender to a Debugger object + var debugger = sender as Debugger; + if (debugger != null) + { + // Enable step mode to allow for command execution during the debug stop + debugger.SetDebuggerStepMode(true); + + PSCommand command = new PSCommand(); + + if (shouldCollectVariables) + { + // Collect variables + command.AddCommand("Get-PodeDumpScopedVariable"); + } + else + { + // Execute a break + command.AddCommand( "while( $PodeContext.Server.Suspended){ Start-sleep 1}"); + } + + // Create a collection to store the command output + PSDataCollection outputCollection = new PSDataCollection(); + + // Execute the command within the debugger + debugger.ProcessCommand(command, outputCollection); + + // Add results to the variables collection if collecting variables + if (shouldCollectVariables) + { + foreach (var output in outputCollection) + { + variables.Add(output); + } + } + else + { + // Ensure the debugger remains ready for further interaction + debugger.SetDebuggerStepMode(true); + } + } + } + + + // Method to check if the DebuggerStop event has been triggered + public static bool IsEventTriggered() + { + return eventTriggered; + } + + // Method to retrieve the collected variables + public static PSDataCollection GetVariables() + { + return variables; + } + } +} \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 36ce9d7e7..70a6d1903 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -43,11 +43,8 @@ function New-PodeContext { [string[]] $EnablePool, - [switch] - $DisableTermination, - - [switch] - $Quiet, + [hashtable] + $Console, [switch] $EnableBreakpoints @@ -92,8 +89,7 @@ function New-PodeContext { $ctx.Server.LogicPath = $FilePath $ctx.Server.Interval = $Interval $ctx.Server.PodeModule = (Get-PodeModuleInfo) - $ctx.Server.DisableTermination = $DisableTermination.IsPresent - $ctx.Server.Quiet = $Quiet.IsPresent + $ctx.Server.Console = $Console $ctx.Server.ComputerName = [System.Net.DNS]::GetHostName() # list of created listeners/receivers @@ -192,7 +188,7 @@ function New-PodeContext { $ctx.Server.Debug = @{ Breakpoints = @{ - Debug = $false + Enabled = $false } Dump = @{ Enabled = $true @@ -237,7 +233,7 @@ function New-PodeContext { $ctx.Server.ServerlessType = $ServerlessType $ctx.Server.IsServerless = $isServerless if ($isServerless) { - $ctx.Server.DisableTermination = $true + $ctx.Server.Console.DisableTermination = $true } # set the server types @@ -247,11 +243,11 @@ function New-PodeContext { # is the server running under IIS? (also, disable termination) $ctx.Server.IsIIS = (!$isServerless -and (!(Test-PodeIsEmpty $env:ASPNETCORE_PORT)) -and (!(Test-PodeIsEmpty $env:ASPNETCORE_TOKEN))) if ($ctx.Server.IsIIS) { - $ctx.Server.DisableTermination = $true + $ctx.Server.Console.DisableTermination = $true # if under IIS and Azure Web App, force quiet if (!(Test-PodeIsEmpty $env:WEBSITE_IIS_SITE_NAME)) { - $ctx.Server.Quiet = $true + $ctx.Server.Console.Quiet = $true } # set iis token/settings @@ -278,7 +274,7 @@ function New-PodeContext { # if we're inside a remote host, stop termination if ($Host.Name -ieq 'ServerRemoteHost') { - $ctx.Server.DisableTermination = $true + $ctx.Server.Console.DisableTermination = $true } # set the IP address details diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 67a5f409c..cafc568e9 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -116,9 +116,7 @@ function Invoke-PodeDumpInternal { # Process block to handle each pipeline input process { # Ensure Dump directory exists in the specified path - if ( $Path -match '^\.{1,2}([\\\/]|$)') { - $Path = [System.IO.Path]::Combine($PodeContext.Server.Root, $Path.Substring(2)) - } + $Path = Get-PodeRelativePath -Path $Path -JoinRoot if (!(Test-Path -Path $Path)) { New-Item -ItemType Directory -Path $Path | Out-Null @@ -128,22 +126,22 @@ function Invoke-PodeDumpInternal { $process = Get-Process -Id $PID $memoryDetails = @( [Ordered]@{ - ProcessId = $process.Id - ProcessName = $process.ProcessName - WorkingSet = [math]::Round($process.WorkingSet64 / 1MB, 2) - PrivateMemory = [math]::Round($process.PrivateMemorySize64 / 1MB, 2) - VirtualMemory = [math]::Round($process.VirtualMemorySize64 / 1MB, 2) + ProcessId = $process.Id + ProcessName = $process.ProcessName + WorkingSetMB = [math]::Round($process.WorkingSet64 / 1MB, 2) + PrivateMemoryMB = [math]::Round($process.PrivateMemorySize64 / 1MB, 2) + VirtualMemoryMB = [math]::Round($process.VirtualMemorySize64 / 1MB, 2) } ) # Capture the code causing the exception - $scriptContext = @() - $exceptionDetails = @() + $scriptContext = $null + $exceptionDetails = $null $stackTrace = '' if ($null -ne $ErrorRecord) { - $scriptContext += [Ordered]@{ + $scriptContext = [Ordered]@{ ScriptName = $ErrorRecord.InvocationInfo.ScriptName Line = $ErrorRecord.InvocationInfo.Line PositionMessage = $ErrorRecord.InvocationInfo.PositionMessage @@ -151,14 +149,14 @@ function Invoke-PodeDumpInternal { # Capture stack trace information if available $stackTrace = if ($ErrorRecord.Exception.StackTrace) { - $ErrorRecord.Exception.StackTrace -split "`n" + $ErrorRecord.Exception.StackTrace } else { 'No stack trace available' } # Capture exception details - $exceptionDetails += [Ordered]@{ + $exceptionDetails = [Ordered]@{ ExceptionType = $ErrorRecord.Exception.GetType().FullName Message = $ErrorRecord.Exception.Message InnerException = if ($ErrorRecord.Exception.InnerException) { $ErrorRecord.Exception.InnerException.Message } else { $null } @@ -171,18 +169,21 @@ function Invoke-PodeDumpInternal { # Check if RunspacePools is not null before iterating $runspacePoolDetails = @() - # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) + # Retrieve all runspaces related to Pode ordered by name $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' } | Sort-Object Name $runspaceDetails = @{} foreach ($r in $runspaces) { $runspaceDetails[$r.Name] = @{ Id = $r.Id - Name = $r.Name + Name = @{ + $r.Name = @{ + ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r + } + } InitialSessionState = $r.InitialSessionState RunspaceStateInfo = $r.RunspaceStateInfo } - $runspaceDetails[$r.Name].ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r } if ($null -ne $PodeContext.RunspacePools) { @@ -219,31 +220,26 @@ function Invoke-PodeDumpInternal { RunspacePools = $runspacePoolDetails Runspace = $runspaceDetails } - + $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').$($Format.ToLower())" # Determine file extension and save format based on selected Format switch ($Format) { 'json' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').json" $dumpInfo | ConvertTo-Json -Depth $MaxDepth -WarningAction SilentlyContinue | Out-File -FilePath $dumpFilePath break } 'clixml' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').clixml" $dumpInfo | Export-Clixml -Path $dumpFilePath break } 'txt' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" $dumpInfo | Out-String | Out-File -FilePath $dumpFilePath break } 'bin' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').bin" [System.IO.File]::WriteAllBytes($dumpFilePath, [System.Text.Encoding]::UTF8.GetBytes([System.Management.Automation.PSSerializer]::Serialize($dumpInfo, $MaxDepth ))) break } 'yaml' { - $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').yaml" $dumpInfo | ConvertTo-PodeYaml -Depth $MaxDepth | Out-File -FilePath $dumpFilePath break } @@ -252,8 +248,8 @@ function Invoke-PodeDumpInternal { Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" } end { - Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump - $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() + + Reset-PodeCancellationToken -Type 'Dump' } } @@ -288,6 +284,61 @@ function Invoke-PodeDumpInternal { Pode #> +function Get-PodeRunspaceVariablesViaDebuggerNew { + param ( + [Parameter(Mandatory)] + [System.Management.Automation.Runspaces.Runspace]$Runspace, + + [Parameter()] + [int]$Timeout = 60 + ) + + # Initialize variables collection + $variables = @() + try { + + # Attach the debugger and break all + $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) + Enable-RunspaceDebug -BreakAll -Runspace $Runspace + + # Wait for the event to be triggered or timeout + $startTime = [DateTime]::UtcNow + Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine + + while (!$debugger.IsEventTriggered) { + Start-Sleep -Milliseconds 500 + Write-PodeHost '.' -NoNewLine + + if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { + Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" + return @{} + } + } + + Write-PodeHost 'Done' + + # Retrieve and output the collected variables from the embedded C# code + $variables = $debugger.Variables + } + catch { + # Log the error details using Write-PodeErrorLog. + # This ensures that any exceptions thrown during the execution are logged appropriately. + $_ | Write-PodeErrorLog + } + finally { + # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. + if ($null -ne $debugger) { + $debugger.Dispose() + } + + # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. + Disable-RunspaceDebug -Runspace $Runspace + } + + return $variables[0] +} + + function Get-PodeRunspaceVariablesViaDebugger { param ( [Parameter(Mandatory)] @@ -344,6 +395,7 @@ function Get-PodeRunspaceVariablesViaDebugger { return $variables[0] } + <# .SYNOPSIS Collects and serializes variables from different scopes (Local, Script, Global). @@ -452,11 +504,13 @@ function ConvertTo-PodeSerializableObject { } catch { Write-PodeHost $_ -ForegroundColor Red + $_ | Write-PodeErrorLog } } } catch { Write-PodeHost $_ -ForegroundColor Red + $_ | Write-PodeErrorLog } return $result } @@ -471,11 +525,13 @@ function ConvertTo-PodeSerializableObject { } catch { Write-PodeHost $_ -ForegroundColor Red + $_ | Write-PodeErrorLog } } } catch { Write-PodeHost $_ -ForegroundColor Red + $_ | Write-PodeErrorLog } return $result } @@ -491,116 +547,8 @@ function ConvertTo-PodeSerializableObject { } function Initialize-PodeDebugHandler { - # Embed C# code to handle the DebuggerStop event - Add-Type @' -using System; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Collections.ObjectModel; - -namespace Pode.Embedded -{ - public class DebuggerHandler - { - // Collection to store variables collected during the debugging session - private static PSDataCollection variables = new PSDataCollection(); - - // Event handler for the DebuggerStop event - private static EventHandler debuggerStopHandler; - - // Flag to indicate whether the DebuggerStop event has been triggered - private static bool eventTriggered = false; - - // Flag to control whether variables should be collected during the DebuggerStop event - private static bool shouldCollectVariables = true; - - // Method to attach the DebuggerStop event handler to the runspace's debugger - public static void AttachDebugger(Runspace runspace, bool collectVariables = true) - { - // Set the collection flag based on the parameter - shouldCollectVariables = collectVariables; - - // Initialize the event handler with the OnDebuggerStop method - debuggerStopHandler = new EventHandler(OnDebuggerStop); - - // Attach the event handler to the DebuggerStop event of the runspace's debugger - runspace.Debugger.DebuggerStop += debuggerStopHandler; - } - - // Method to detach the DebuggerStop event handler from the runspace's debugger - public static void DetachDebugger(Runspace runspace) - { - if (debuggerStopHandler != null) - { - // Remove the event handler to prevent further event handling - runspace.Debugger.DebuggerStop -= debuggerStopHandler; - - // Set the handler to null to clean up - debuggerStopHandler = null; - } - } - - // Event handler method that gets called when the debugger stops - private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) - { - // Set the eventTriggered flag to true - eventTriggered = true; - - // Cast the sender to a Debugger object - var debugger = sender as Debugger; - if (debugger != null) - { - // Enable step mode to allow for command execution during the debug stop - debugger.SetDebuggerStepMode(true); - - PSCommand command = new PSCommand(); - - if (shouldCollectVariables) - { - // Collect variables - command.AddCommand("Get-PodeDumpScopedVariable"); - } - else - { - // Execute a break - command.AddCommand( "while( $PodeContext.Server.Suspended){ Start-sleep 1}"); - } - - // Create a collection to store the command output - PSDataCollection outputCollection = new PSDataCollection(); - - // Execute the command within the debugger - debugger.ProcessCommand(command, outputCollection); - - // Add results to the variables collection if collecting variables - if (shouldCollectVariables) - { - foreach (var output in outputCollection) - { - variables.Add(output); - } - } - else - { - // Ensure the debugger remains ready for further interaction - debugger.SetDebuggerStepMode(true); - } - } - } - - - // Method to check if the DebuggerStop event has been triggered - public static bool IsEventTriggered() - { - return eventTriggered; - } - - // Method to retrieve the collected variables - public static PSDataCollection GetVariables() - { - return variables; - } + if ($PodeContext.Server.Debug.Dump) { + # Embed C# code to handle the DebuggerStop event + Add-Type -LiteralPath ([System.IO.Path]::Combine((Get-PodeModuleRootPath), 'Embedded', 'DebuggerHandler.cs')) -ErrorAction Stop } -} -'@ } \ No newline at end of file diff --git a/src/Private/Endpoints.ps1 b/src/Private/Endpoints.ps1 index 2ceda786b..54edbdcf3 100644 --- a/src/Private/Endpoints.ps1 +++ b/src/Private/Endpoints.ps1 @@ -398,6 +398,9 @@ function Get-PodeEndpointByName { such as `DualMode`. It provides a summary of the total number of endpoints and the number of general threads handling them. +.PARAMETER Force + Overrides the -Quiet flag of the server. + .EXAMPLE Show-PodeEndPointConsoleInfo @@ -410,24 +413,29 @@ function Get-PodeEndpointByName { enhancing visibility of specific configurations like `DualMode`. #> function Show-PodeEndPointConsoleInfo { - if ($PodeContext.Server.EndpointsInfo.Length -gt 0) { - - # Listening on the following $endpoints.Length endpoint(s) [$PodeContext.Threads.General thread(s)] - Write-PodeHost ($PodeLocale.listeningOnEndpointsMessage -f $PodeContext.Server.EndpointsInfo.Length, $PodeContext.Threads.General) -ForegroundColor Yellow - $PodeContext.Server.EndpointsInfo | ForEach-Object { - $flags = @() - if ($_.DualMode) { - $flags += 'DualMode' - } + param( + [switch] + $Force + ) + if ($PodeContext.Server.EndpointsInfo.Length -eq 0) { + return + } - if ($flags.Length -eq 0) { - $flags = [string]::Empty - } - else { - $flags = "[$($flags -join ',')]" - } + # Listening on the following $endpoints.Length endpoint(s) [$PodeContext.Threads.General thread(s)] + Write-PodeHost ($PodeLocale.listeningOnEndpointsMessage -f $PodeContext.Server.EndpointsInfo.Length, $PodeContext.Threads.General) -ForegroundColor Yellow -Force:$Force + $PodeContext.Server.EndpointsInfo | ForEach-Object { + $flags = @() + if ($_.DualMode) { + $flags += 'DualMode' + } - Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow + if ($flags.Length -eq 0) { + $flags = [string]::Empty } + else { + $flags = "[$($flags -join ',')]" + } + + Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow -Force:$Force } } \ No newline at end of file diff --git a/src/Private/FileMonitor.ps1 b/src/Private/FileMonitor.ps1 index 7b9fae009..174b27194 100644 --- a/src/Private/FileMonitor.ps1 +++ b/src/Private/FileMonitor.ps1 @@ -83,7 +83,7 @@ function Start-PodeFileMonitor { } -MessageData @{ Tokens = $PodeContext.Tokens FileSettings = $PodeContext.Server.FileMonitor - Quiet = $PodeContext.Server.Quiet + Quiet = $PodeContext.Server.Console.Quiet } -SupportEvent } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 59d91022c..53aac4379 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -566,7 +566,7 @@ function Test-PodeTerminationPressed { $Key = $null ) - if ($PodeContext.Server.DisableTermination) { + if ($PodeContext.Server.Console.DisableConsoleInput -or $PodeContext.Server.Console.DisableTermination) { return $false } @@ -579,7 +579,7 @@ function Test-PodeRestartPressed { $Key = $null ) - if ($PodeContext.Server.DisableTermination) { + if ($PodeContext.Server.Console.DisableConsoleInput) { return $false } @@ -592,20 +592,85 @@ function Test-PodeOpenBrowserPressed { $Key = $null ) - if ($PodeContext.Server.DisableTermination) { + if ($PodeContext.Server.Console.DisableConsoleInput) { return $false } return (Test-PodeKeyPressed -Key $Key -Character 'b') } +function Test-PodeHelpPressed { + param( + [Parameter()] + $Key = $null + ) + + if ($PodeContext.Server.Console.DisableConsoleInput) { + return $false + } + + return (Test-PodeKeyPressed -Key $Key -Character 'h') +} + +function Test-PodeOpenAPIPressed { + param( + [Parameter()] + $Key = $null + ) + + if ($PodeContext.Server.Console.DisableConsoleInput) { + return $false + } + + return (Test-PodeKeyPressed -Key $Key -Character 'o') +} + +function Test-PodeEndpointsPressed { + param( + [Parameter()] + $Key = $null + ) + + if ($PodeContext.Server.Console.DisableConsoleInput) { + return $false + } + + return (Test-PodeKeyPressed -Key $Key -Character 'e') +} + +function Test-PodeClearPressed { + param( + [Parameter()] + $Key = $null + ) + + if ($PodeContext.Server.Console.DisableConsoleInput) { + return $false + } + + return (Test-PodeKeyPressed -Key $Key -Character 'l') +} + +function Test-PodeQuietPressed { + param( + [Parameter()] + $Key = $null + ) + + if ($PodeContext.Server.Console.DisableConsoleInput) { + return $false + } + + return (Test-PodeKeyPressed -Key $Key -Character 't') +} + function Test-PodeDumpPressed { param( [Parameter()] $Key = $null ) - if ($PodeContext.Server.DisableTermination) { + if ($PodeContext.Server.Console.DisableConsoleInput) { return $false } return (Test-PodeKeyPressed -Key $Key -Character 'd') @@ -617,7 +682,7 @@ function Test-PodeSuspendPressed { $Key = $null ) - if ($PodeContext.Server.DisableTermination) { + if ($PodeContext.Server.Console.DisableConsoleInput -or $PodeContext.Server.Console.DisableTermination) { return $false } @@ -646,14 +711,13 @@ function Test-PodeSuspendPressed { #> function Clear-PodeKeyPressed { - if ($PodeContext.Server.DisableTermination) { - return $false - } + if (!$PodeContext.Server.Console.DisableConsoleInput) { - # Clear any remaining keys in the input buffer - while ([Console]::KeyAvailable) { + # Clear any remaining keys in the input buffer + while ([Console]::KeyAvailable) { - [Console]::ReadKey($true) | Out-Null + [Console]::ReadKey($true) | Out-Null + } } } @@ -2558,7 +2622,7 @@ function Get-PodeRelativePath { $RootPath = $PodeContext.Server.Root } - $Path = [System.IO.Path]::Combine($RootPath, $Path) + $Path = [System.IO.Path]::Combine($RootPath, $Path.Substring(2)) } # if flagged, resolve the path diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index f948a140d..0f431798c 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -2384,6 +2384,9 @@ function Test-PodeRouteOADefinitionTag { documentation endpoints in the console. The information includes protocol, address, and paths for specification and documentation endpoints. +.PARAMETER Force + Overrides the -Quiet flag of the server. + .EXAMPLE Show-PodeOAConsoleInfo @@ -2394,47 +2397,63 @@ function Test-PodeRouteOADefinitionTag { This is an internal function and may change in future releases of Pode. #> function Show-PodeOAConsoleInfo { + param( + [switch] + $Force + ) # state the OpenAPI endpoints for each definition foreach ($key in $PodeContext.Server.OpenAPI.Definitions.keys) { $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks - if ( $bookmarks) { - Write-PodeHost - if (!$OpenAPIHeader) { - # OpenAPI Info - Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor Green - $OpenAPIHeader = $true - } - Write-PodeHost " '$key':" -ForegroundColor Yellow + if ( !$bookmarks) { + continue + } - if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { - # Specification - Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow - foreach ($endpoint in $bookmarks.route.Endpoint) { - Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor White - } - # Documentation - Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow - foreach ($endpoint in $bookmarks.route.Endpoint) { - Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor White - } + Write-PodeHost -Force:$Force + if (!$OpenAPIHeader) { + # OpenAPI Info + Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor Green -Force:$Force + $OpenAPIHeader = $true + } + Write-PodeHost " '$key':" -ForegroundColor Yellow -Force:$Force + + if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow -Force:$Force + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor White -Force:$Force } - else { - # Specification - Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow - $PodeContext.Server.EndpointsInfo | ForEach-Object { - if ($_.Pool -eq 'web') { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) - Write-PodeHost " . $url" -ForegroundColor White - } + # Documentation + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow -Force:$Force + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor White -Force:$Force + } + } + else { + # Specification + Write-PodeHost " - $($PodeLocale.specificationMessage):" -ForegroundColor Yellow -Force:$Force + $PodeContext.Server.EndpointsInfo | ForEach-Object { + if ($_.Pool -eq 'web') { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.openApiUrl) + Write-PodeHost " . $url" -ForegroundColor White -Force:$Force } - Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow - $PodeContext.Server.EndpointsInfo | ForEach-Object { - if ($_.Pool -eq 'web') { - $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) - Write-PodeHost " . $url" -ForegroundColor White - } + } + Write-PodeHost " - $($PodeLocale.documentationMessage):" -ForegroundColor Yellow -Force:$Force + $PodeContext.Server.EndpointsInfo | ForEach-Object { + if ($_.Pool -eq 'web') { + $url = [System.Uri]::new( [System.Uri]::new($_.Url), $bookmarks.path) + Write-PodeHost " . $url" -ForegroundColor White -Force:$Force } } } } +} + +function Test-PodeOAEnabled { + foreach ($key in $PodeContext.Server.OpenAPI.Definitions.keys) { + $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks + if ( $bookmarks) { + return $true + } + } + return $false } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index c93cd77fa..67d8e20fe 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -12,10 +12,10 @@ function Start-PodeInternalServer { Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID)) " -ForegroundColor Cyan -NoNewline if ($PodeContext.Metrics.Server.RestartCount -gt 0) { - Write-PodeHost "[$( $PodeLocale.restartingMessage)]" -ForegroundColor Cyan + Write-PodeHost "[$( $PodeLocale.restartingMessage)]" -ForegroundColor Cyan -NoNewline } else { - Write-PodeHost "[$($PodeLocale.initializingMessage)]" -ForegroundColor Cyan + Write-PodeHost "[$($PodeLocale.initializingMessage)]" -ForegroundColor Cyan -NoNewline } $null = Test-PodeVersionPwshEOL -ReportUntested @@ -155,7 +155,7 @@ function Start-PodeInternalServer { # run running event hooks Invoke-PodeEvent -Type Running - Show-PodeConsoleInfo -ClearHost -ShowHeader + Show-PodeConsoleInfo -ShowHeader } catch { @@ -188,14 +188,17 @@ function Show-PodeConsoleInfo { $ClearHost, [switch] - $ShowHeader + $ShowHeader, + + [switch] + $Force ) - if ($PodeContext.Server.Quiet) { + if ($PodeContext.Server.Console.Quiet -and !$Force) { return } - if ($ClearHost) { + if ($ClearHost -or $PodeContext.Server.Console.ClearHost) { Clear-Host } @@ -207,32 +210,50 @@ function Show-PodeConsoleInfo { else { $status = $Podelocale.runningMessage # Running } - Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID)) [$status]" -ForegroundColor Cyan + + Write-PodeHost "`rPode $(Get-PodeVersion) (PID: $($PID)) [$status] " -ForegroundColor Cyan -Force:$Force -NoNewLine + if ($PodeContext.Server.Console.ShowHelp -or $Force) { + Write-PodeHost -Force:$Force + }else{ + Write-PodeHost '- Ctrl+H for the Command List' -ForegroundColor Cyan + } } if (!$PodeContext.Server.Suspended) { - # state what endpoints are being listened on - Show-PodeEndPointConsoleInfo - - # state the OpenAPI endpoints for each definition - Show-PodeOAConsoleInfo + if ($PodeContext.Server.Console.ShowEndpoints) { + # state what endpoints are being listened on + Show-PodeEndPointConsoleInfo -Force:$Force + } + if ($PodeContext.Server.Console.ShowOpenAPI) { + # state the OpenAPI endpoints for each definition + Show-PodeOAConsoleInfo -Force:$Force + } } - if (!$PodeContext.Server.DisableTermination) { + if (!$PodeContext.Server.Console.DisableConsoleInput -and $PodeContext.Server.Console.ShowHelp) { $resumeOrSuspend = $(if ($PodeContext.Server.Suspended) { $Podelocale.ResumeServerMessage } else { $Podelocale.SuspendServerMessage }) - Write-PodeHost - Write-PodeHost $Podelocale.ServerControlCommandsTitle -ForegroundColor Green - Write-PodeHost " Ctrl+C : $($Podelocale.GracefullyTerminateMessage)" -ForegroundColor Cyan - Write-PodeHost " Ctrl+R : $($Podelocale.RestartServerMessage)" -ForegroundColor Cyan - Write-PodeHost " Ctrl+U : $resumeOrSuspend" -ForegroundColor Cyan + Write-PodeHost -Force:$Force + Write-PodeHost $Podelocale.ServerControlCommandsTitle -ForegroundColor Green -Force:$Force + Write-PodeHost " Ctrl+C : $($Podelocale.GracefullyTerminateMessage)" -ForegroundColor Cyan -Force:$Force + Write-PodeHost " Ctrl+R : $($Podelocale.RestartServerMessage)" -ForegroundColor Cyan -Force:$Force + Write-PodeHost " Ctrl+U : $resumeOrSuspend" -ForegroundColor Cyan -Force:$Force if ((Get-PodeEndpointUrl) -and !($PodeContext.Server.Suspended)) { - Write-PodeHost " Ctrl+B : $($Podelocale.OpenHttpEndpointMessage)" -ForegroundColor Cyan + Write-PodeHost " Ctrl+B : $($Podelocale.OpenHttpEndpointMessage)" -ForegroundColor Cyan -Force:$Force } if ($PodeContext.Server.Debug.Dump.Enabled) { - Write-PodeHost " Ctrl+D : $($Podelocale.GenerateDiagnosticDumpMessage)" -ForegroundColor Cyan + Write-PodeHost " Ctrl+D : $($Podelocale.GenerateDiagnosticDumpMessage)" -ForegroundColor Cyan -Force:$Force } + Write-PodeHost ' ----' -ForegroundColor Cyan -Force:$Force + Write-PodeHost ' Ctrl+H : Hide this help' -ForegroundColor Cyan -Force:$Force + Write-PodeHost " Ctrl+E : $(if($PodeContext.Server.Console.ShowEndpoints){'Hide'}else{'Show'}) Endpoints" -ForegroundColor Cyan -Force:$Force + if (Test-PodeOAEnabled) { + Write-PodeHost " Ctrl+O : $(if($PodeContext.Server.Console.ShowOpenAPI){'Hide'}else{'Show'}) OpenAPI" -ForegroundColor Cyan -Force:$Force + } + Write-PodeHost ' Ctrl+L : Clear the Console' -ForegroundColor Cyan -Force:$Force + + Write-PodeHost " Ctrl+T : $(if($PodeContext.Server.Console.Quiet){'Disable'}else{'Enable'}) Quiet Mode" -ForegroundColor Cyan -Force:$Force } } @@ -352,14 +373,9 @@ function Restart-PodeInternalServer { $PodeContext.Server.Types = @() # recreate the session tokens - Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation - $PodeContext.Tokens.Cancellation = [System.Threading.CancellationTokenSource]::new() - - Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart - $PodeContext.Tokens.Restart = [System.Threading.CancellationTokenSource]::new() - - Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump - $PodeContext.Tokens.Dump = [System.Threading.CancellationTokenSource]::new() + Reset-PodeCancellationToken -Type Cancellation + Reset-PodeCancellationToken -Type Restart + Reset-PodeCancellationToken -Type Dump # reload the configuration $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext @@ -380,6 +396,72 @@ function Restart-PodeInternalServer { } } + +<# +.SYNOPSIS + Resets the cancellation token for a specific type in Pode. + +.DESCRIPTION + The `Reset-PodeCancellationToken` function disposes of the existing cancellation token + for the specified type and reinitializes it with a new token. This ensures proper cleanup + of disposable resources associated with the cancellation token. + +.PARAMETER Type + The type of cancellation token to reset. This is a mandatory parameter and must be + provided as a string. + +.EXAMPLES + # Reset the cancellation token for the 'Cancellation' type + Reset-PodeCancellationToken -Type Cancellation + + # Reset the cancellation token for the 'Restart' type + Reset-PodeCancellationToken -Type Restart + + # Reset the cancellation token for the 'Dump' type + Reset-PodeCancellationToken -Type Dump + + # Reset the cancellation token for the 'SuspendResume' type + Reset-PodeCancellationToken -Type SuspendResume + +.NOTES + This function is used to manage cancellation tokens in Pode's internal context. + +#> +function Reset-PodeCancellationToken { + param( + [Parameter(Mandatory = $true)] + [string] + $Type + ) + # Ensure cleanup of disposable tokens + Close-PodeDisposable -Disposable $PodeContext.Tokens[$Type] + + # Reinitialize the Token + $PodeContext.Tokens[$Type] = [System.Threading.CancellationTokenSource]::new() +} + + +<# +.SYNOPSIS + Determines whether the Pode server should remain open based on its configuration and active components. + +.DESCRIPTION + The `Test-PodeServerKeepOpen` function evaluates the current server state and configuration + to decide whether to keep the Pode server running. It considers the existence of timers, + schedules, file watchers, service mode, and server types to make this determination. + + - If any timers, schedules, or file watchers are active, the server remains open. + - If the server is not running as a service and is either serverless or has no types defined, + the server will close. + - In other cases, the server will stay open. + + .NOTES + This function is primarily used internally by Pode to manage the server lifecycle. + It helps ensure the server remains active only when necessary based on its current state. + + +#> + function Test-PodeServerKeepOpen { # if we have any timers/schedules/fim - keep open if ((Test-PodeTimersExist) -or (Test-PodeSchedulesExist) -or (Test-PodeFileWatchersExist)) { @@ -406,14 +488,14 @@ function Test-PodeServerKeepOpen { .PARAMETER Timeout The maximum time, in seconds, to wait for each runspace to be suspended before timing out. Default is 30 seconds. -.NOTES - This is an internal function used within the Pode framework. - It may change in future releases. - .EXAMPLE Suspend-PodeServerInternal -Timeout 60 # Suspends the Pode server with a timeout of 60 seconds. +.NOTES + This is an internal function used within the Pode framework. + It may change in future releases. + #> function Suspend-PodeServerInternal { param( @@ -472,18 +554,14 @@ function Suspend-PodeServerInternal { Start-Sleep -Seconds 5 # Clear the host and display header information - Show-PodeConsoleInfo -ClearHost -ShowHeader + Show-PodeConsoleInfo -ShowHeader } catch { # Log any errors that occur $_ | Write-PodeErrorLog } finally { - # Ensure cleanup of disposable tokens - Close-PodeDisposable -Disposable $PodeContext.Tokens.SuspendResume - - # Reinitialize the CancellationTokenSource for future suspension/resumption - $PodeContext.Tokens.SuspendResume = [System.Threading.CancellationTokenSource]::new() + Reset-PodeCancellationToken -Type SuspendResume } } @@ -533,17 +611,14 @@ function Resume-PodeServerInternal { Start-Sleep 1 # Clear the host and display header information - Show-PodeConsoleInfo -ClearHost -ShowHeader + Show-PodeConsoleInfo -ShowHeader } catch { # Log any errors that occur $_ | Write-PodeErrorLog } finally { - # Ensure cleanup of disposable tokens - Close-PodeDisposable -Disposable $PodeContext.Tokens.SuspendResume - # Reinitialize the CancellationTokenSource for future suspension/resumption - $PodeContext.Tokens.SuspendResume = [System.Threading.CancellationTokenSource]::new() + Reset-PodeCancellationToken -Type SuspendResume } } \ No newline at end of file diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index cb4aa0ee7..815de45bb 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -1,69 +1,75 @@ <# .SYNOPSIS -Starts a Pode Server with the supplied ScriptBlock. + Starts a Pode Server with the supplied ScriptBlock. .DESCRIPTION -Starts a Pode Server with the supplied ScriptBlock. + Starts a Pode Server with the supplied ScriptBlock. .PARAMETER ScriptBlock -The main logic for the Server. + The main logic for the Server. .PARAMETER FilePath -A literal, or relative, path to a file containing a ScriptBlock for the Server's logic. -The directory of this file will be used as the Server's root path - unless a specific -RootPath is supplied. + A literal, or relative, path to a file containing a ScriptBlock for the Server's logic. + The directory of this file will be used as the Server's root path - unless a specific -RootPath is supplied. .PARAMETER Interval -For 'Service' type Servers, will invoke the ScriptBlock every X seconds. + For 'Service' type Servers, will invoke the ScriptBlock every X seconds. .PARAMETER Name -An optional name for the Server (intended for future ideas). + An optional name for the Server (intended for future ideas). .PARAMETER Threads -The numbers of threads to use for Web, SMTP, and TCP servers. + The numbers of threads to use for Web, SMTP, and TCP servers. .PARAMETER RootPath -An override for the Server's root path. + An override for the Server's root path. .PARAMETER Request -Intended for Serverless environments, this is Requests details that Pode can parse and use. + Intended for Serverless environments, this is Requests details that Pode can parse and use. .PARAMETER ServerlessType -Optional, this is the serverless type, to define how Pode should run and deal with incoming Requests. + Optional, this is the serverless type, to define how Pode should run and deal with incoming Requests. .PARAMETER StatusPageExceptions -An optional value of Show/Hide to control where Stacktraces are shown in the Status Pages. -If supplied this value will override the ShowExceptions setting in the server.psd1 file. + An optional value of Show/Hide to control where Stacktraces are shown in the Status Pages. + If supplied this value will override the ShowExceptions setting in the server.psd1 file. .PARAMETER ListenerType -An optional value to use a custom Socket Listener. The default is Pode's inbuilt listener. -There's the Pode.Kestrel module, so the value here should be "Kestrel" if using that. + An optional value to use a custom Socket Listener. The default is Pode's inbuilt listener. + There's the Pode.Kestrel module, so the value here should be "Kestrel" if using that. .PARAMETER DisableTermination -Disables the ability to terminate the Server. + Disables the ability to terminate and suspend/resume the Server. + +.PARAMETER DisableConsoleInput + Disables any console keyboard interaction + +.PARAMETER ClearHost + Clears the console screen before displaying server information. .PARAMETER Quiet -Disables any output from the Server. + Disables any output from the Server. .PARAMETER Browse -Open the web Server's default endpoint in your default browser. + Open the web Server's default endpoint in your default browser. .PARAMETER CurrentPath -Sets the Server's root path to be the current working path - for -FilePath only. + Sets the Server's root path to be the current working path - for -FilePath only. .PARAMETER EnablePool -Tells Pode to configure certain RunspacePools when they're being used adhoc, such as Timers or Schedules. + Tells Pode to configure certain RunspacePools when they're being used adhoc, such as Timers or Schedules. .PARAMETER EnableBreakpoints -If supplied, any breakpoints created by using Wait-PodeDebugger will be enabled - or disabled if false passed explicitly, or not supplied. + If supplied, any breakpoints created by using Wait-PodeDebugger will be enabled - or disabled if false passed explicitly, or not supplied. .EXAMPLE -Start-PodeServer { /* logic */ } + Start-PodeServer { /* logic */ } .EXAMPLE -Start-PodeServer -Interval 10 { /* logic */ } + Start-PodeServer -Interval 10 { /* logic */ } .EXAMPLE -Start-PodeServer -Request $LambdaInput -ServerlessType AwsLambda { /* logic */ } + Start-PodeServer -Request $LambdaInput -ServerlessType AwsLambda { /* logic */ } #> function Start-PodeServer { [CmdletBinding(DefaultParameterSetName = 'Script')] @@ -117,6 +123,12 @@ function Start-PodeServer { [switch] $DisableTermination, + [switch] + $DisableConsoleInput, + + [switch] + $ClearHost, + [switch] $Quiet, @@ -146,9 +158,6 @@ function Start-PodeServer { # Sets the name of the current runspace Set-PodeCurrentRunspaceName -Name 'PodeServer' - # Compile the Debug Handler - Initialize-PodeDebugHandler - # ensure the session is clean $Script:PodeContext = $null $ShowDoneMessage = $true @@ -174,23 +183,39 @@ function Start-PodeServer { $RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath } - # create main context object - $PodeContext = New-PodeContext ` - -ScriptBlock $ScriptBlock ` - -FilePath $FilePath ` - -Threads $Threads ` - -Interval $Interval ` - -ServerRoot (Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) ` - -ServerlessType $ServerlessType ` - -ListenerType $ListenerType ` - -EnablePool $EnablePool ` - -StatusPageExceptions $StatusPageExceptions ` - -DisableTermination:$DisableTermination ` - -Quiet:$Quiet ` - -EnableBreakpoints:$EnableBreakpoints + + # Define parameters for the context creation + $ContextParams = @{ + ScriptBlock = $ScriptBlock + FilePath = $FilePath + Threads = $Threads + Interval = $Interval + ServerRoot = Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot + ServerlessType = $ServerlessType + ListenerType = $ListenerType + EnablePool = $EnablePool + StatusPageExceptions = $StatusPageExceptions + Console = @{ + DisableTermination = $DisableTermination.IsPresent + DisableConsoleInput = $DisableConsoleInput.IsPresent + Quiet = $Quiet.IsPresent + ClearHost = $ClearHost.IsPresent + ShowOpenAPI = $true + ShowEndpoints = $true + ShowHelp = $false + + } + EnableBreakpoints = $EnableBreakpoints + } + + # Create main context object + $PodeContext = New-PodeContext @ContextParams + + # Compile the Debug Handler + Initialize-PodeDebugHandler # set it so ctrl-c can terminate, unless serverless/iis, or disabled - if (!$PodeContext.Server.DisableTermination -and ($null -eq $psISE)) { + if (!$PodeContext.Server.Console.DisableTermination -and ($null -eq $psISE)) { [Console]::TreatControlCAsInput = $true } @@ -209,7 +234,7 @@ function Start-PodeServer { while ( !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) { Start-Sleep -Seconds 1 - if (!$PodeContext.Server.DisableTermination) { + if (!$PodeContext.Server.Console.DisableConsoleInput) { # get the next key presses $key = Get-PodeConsoleKey } @@ -222,7 +247,7 @@ function Start-PodeServer { if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { Clear-PodeKeyPressed - Invoke-PodeDumpInternal + Invoke-PodeDumpInternal -Format $PodeContext.Server.Debug.Dump.Format -Path $PodeContext.Server.Debug.Dump.Path -MaxDepth $PodeContext.Server.Debug.Dump.MaxDepth if ($PodeContext.Server.Debug.Dump.Param.Halt) { Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' break @@ -243,13 +268,43 @@ function Start-PodeServer { if (Test-PodeOpenBrowserPressed -Key $key) { Clear-PodeKeyPressed $url = Get-PodeEndpointUrl - if ($null -ne $url) { + if (![string]::IsNullOrWhitespace($url)) { Invoke-PodeEvent -Type Browser Start-Process $url } } - if (Test-PodeTerminationPressed -Key $key) { + if ( Test-PodeHelpPressed -Key $key) { + Clear-PodeKeyPressed + $PodeContext.Server.Console.ShowHelp = !$PodeContext.Server.Console.ShowHelp + Show-PodeConsoleInfo -ShowHeader -ClearHost + } + + if ( Test-PodeOpenAPIPressed -Key $key) { + Clear-PodeKeyPressed + $PodeContext.Server.Console.ShowOpenAPI = !$PodeContext.Server.Console.ShowOpenAPI + Show-PodeConsoleInfo -ShowHeader -ClearHost + } + + if ( Test-PodeEndpointsPressed -Key $key) { + Clear-PodeKeyPressed + $PodeContext.Server.Console.ShowEndpoints = !$PodeContext.Server.Console.ShowEndpoints + Show-PodeConsoleInfo -ShowHeader -ClearHost + } + + if ( Test-PodeClearPressed -Key $key) { + Clear-PodeKeyPressed + Show-PodeConsoleInfo -ShowHeader -ClearHost + } + + if ( Test-PodeQuietPressed -Key $key) { + Clear-PodeKeyPressed + $PodeContext.Server.Console.Quiet = !$PodeContext.Server.Console.Quiet + Show-PodeConsoleInfo -ShowHeader -ClearHost -Force + } + + if ( Test-PodeTerminationPressed -Key $key) { + Clear-PodeKeyPressed break } } @@ -259,7 +314,7 @@ function Start-PodeServer { Write-PodeHost $PodeLocale.iisShutdownMessage -NoNewLine -ForegroundColor Yellow Write-PodeHost ' ' -NoNewLine } - + # Terminating... Write-PodeHost $PodeLocale.terminatingMessage -NoNewLine -ForegroundColor Yellow Invoke-PodeEvent -Type Terminate @@ -269,7 +324,8 @@ function Start-PodeServer { $_ | Write-PodeErrorLog if ($PodeContext.Server.Debug.Dump.Enable) { - Invoke-PodeDumpInternal -ErrorRecord $_ + Invoke-PodeDumpInternal -ErrorRecord $_ -Format $PodeContext.Server.Debug.Dump.Format ` + -Path $PodeContext.Server.Debug.Dump.Path -MaxDepth $PodeContext.Server.Debug.Dump.MaxDepth } Invoke-PodeEvent -Type Crash @@ -362,11 +418,8 @@ function Resume-PodeServer { This function suspends the Pode server by pausing all associated runspaces and ensuring they enter a debug state. It triggers the 'Suspend' event, updates the server's suspended status, and provides feedback during the suspension process. -.PARAMETER Timeout - The maximum time, in seconds, to wait for each runspace to be suspended before timing out. Default is 30 seconds. - .EXAMPLE - Suspend-PodeServerInternal -Timeout 60 + Suspend-PodeServer # Suspends the Pode server with a timeout of 60 seconds. #> diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index a33f2e604..e19c9243d 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -814,7 +814,7 @@ function Out-PodeHost { } end { - if ($PodeContext.Server.Quiet) { + if ($PodeContext.Server.Console.Quiet) { return } # Set InputObject to the array of values @@ -855,6 +855,9 @@ Show the Object Type .PARAMETER Label Show a label for the object +.PARAMETER Force +Overrides the -Quiet flag of the server. + .EXAMPLE 'Some output' | Write-PodeHost -ForegroundColor Cyan #> @@ -883,7 +886,10 @@ function Write-PodeHost { [Parameter( Mandatory = $false, ParameterSetName = 'object')] [string] - $Label + $Label, + + [switch] + $Force ) begin { # Initialize an array to hold piped-in values @@ -896,7 +902,7 @@ function Write-PodeHost { } end { - if ($PodeContext.Server.Quiet) { + if ($PodeContext.Server.Console.Quiet -and !($Force.IsPresent)) { return } # Set Object to the array of values @@ -1549,7 +1555,7 @@ function Invoke-PodeDump { $ErrorRecord, [Parameter()] - [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] + [ValidateSet('JSON', 'CLIXML', 'TXT', 'BIN', 'YAML')] [string] $Format, diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 8e1be9266..0b756a58d 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -55,7 +55,7 @@ Describe 'Start-PodeInternalServer' { } It 'Calls smtp server logic' { - $PodeContext.Server = @{ Types = 'SMTP'; Logic = {}; Quiet = $true } + $PodeContext.Server = @{ Types = 'SMTP'; Logic = {}; Quiet = $true } Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -69,7 +69,7 @@ Describe 'Start-PodeInternalServer' { } It 'Calls tcp server logic' { - $PodeContext.Server = @{ Types = 'TCP'; Logic = {}; Quiet = $true } + $PodeContext.Server = @{ Types = 'TCP'; Logic = {}; Quiet = $true } Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -83,7 +83,7 @@ Describe 'Start-PodeInternalServer' { } It 'Calls http web server logic' { - $PodeContext.Server = @{ Types = 'HTTP'; Logic = {}; Quiet = $true } + $PodeContext.Server = @{ Types = 'HTTP'; Logic = {}; Quiet = $true } Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -112,8 +112,10 @@ Describe 'Restart-PodeInternalServer' { It 'Resetting the server values' { $PodeContext = @{ Tokens = @{ - Cancellation = [System.Threading.CancellationTokenSource]::new() - Restart = [System.Threading.CancellationTokenSource]::new() + Cancellation = [System.Threading.CancellationTokenSource]::new() + Restart = [System.Threading.CancellationTokenSource]::new() + Dump = [System.Threading.CancellationTokenSource]::new() + SuspendResume = [System.Threading.CancellationTokenSource]::new() } Server = @{ Routes = @{ @@ -203,7 +205,7 @@ Describe 'Restart-PodeInternalServer' { Storage = @{} } ScopedVariables = @{} - Quiet = $true + Quiet = $true } Metrics = @{ Server = @{ From c8ab57c1ed1dca93492dcf55a9f7733ed86ce467 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 1 Dec 2024 08:18:08 -0800 Subject: [PATCH 20/34] second update --- docs/Getting-Started/Debug.md | 73 ++++++++++++++++-- docs/Getting-Started/FirstApp.md | 2 +- src/Embedded/DebuggerHandler.cs | 120 +++++++++++++---------------- src/Embedded/NewDebuggerHandler.cs | 97 ----------------------- src/Embedded/OldDebuggerHandler.cs | 109 -------------------------- src/Private/Dump.ps1 | 83 ++------------------ src/Private/Helpers.ps1 | 2 +- src/Public/Core.ps1 | 4 - src/Public/Utilities.ps1 | 25 +----- tests/unit/Helpers.Tests.ps1 | 4 +- 10 files changed, 133 insertions(+), 386 deletions(-) delete mode 100644 src/Embedded/NewDebuggerHandler.cs delete mode 100644 src/Embedded/OldDebuggerHandler.cs diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index f53098f8e..d76869af1 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -214,7 +214,7 @@ Set-PodeCurrentRunspaceName -Name 'Custom Runspace Name' This cmdlet sets a custom name for the runspace, making it easier to track during execution. -#### Example +#### Example: Setting a Custom Runspace Name in a Task Here’s an example that demonstrates how to set a custom runspace name in a Pode task: @@ -240,7 +240,7 @@ Get-PodeCurrentRunspaceName This cmdlet returns the name of the current runspace, allowing for easier tracking and management in complex scenarios with multiple concurrent runspaces. -#### Example +#### Example: Outputting the Runspace Name in a Schedule Here’s an example that uses `Get-PodeCurrentRunspaceName` to output the runspace name during the execution of a schedule: @@ -291,7 +291,7 @@ try { } catch { # Capture the dump with custom options - Invoke-PodeDump -ErrorRecord $_ -Format 'clixml' -Path 'C:\CustomDump' -Halt + Invoke-PodeDump -ErrorRecord $_ -Format 'clixml' -Path 'C:\CustomDump' } ``` @@ -299,7 +299,6 @@ In this example: - The memory dump is saved in CLIXML format instead of the default. - The dump file is saved in the specified directory (`C:\CustomDump`) instead of the default path. -- The `-Halt` switch will terminate the application after the dump is saved. ### Using the Dump Feature @@ -319,13 +318,72 @@ Start-PodeServer -EnableBreakpoints { } catch { # Invoke a memory dump when a critical error occurs - $_ | Invoke-PodeDump -Halt + $_ | Invoke-PodeDump } } } ``` -In this setup, if an error occurs in the route, `Invoke-PodeDump` is called, capturing the current state and halting the application if the `-Halt` switch is set. +In this setup, if an error occurs in the route, `Invoke-PodeDump` is called, capturing the current state. + +### Dump File Content + +When a dump file is created using the `Invoke-PodeDump` function, it captures detailed information about the state of the Pode application. The captured information provides valuable insights into the internal state at the time of the error or diagnostic event. Below is a description of the content included in the dump file: + +#### **1. Timestamp** + +- The exact date and time when the dump was generated. +- Format: ISO8601 (e.g., `2024-12-01T12:34:56`). + +#### **2. Memory Details** + +- Provides information about the memory usage of the Pode application at the time of the dump. +- Captures metrics such as total memory allocated and other relevant details to analyze memory-related issues. + +#### **3. Script Context** + +- Contains details about the script execution context at the time of the dump. +- Includes information such as the script path, parameters passed, and the execution environment. + +#### **4. Stack Trace** + +- A snapshot of the call stack, showing the sequence of function calls leading up to the dump. +- Useful for identifying the source of an error or pinpointing the exact location in the code where the issue occurred. + +#### **5. Exception Details** + +- Contains information about any exceptions that triggered the dump. +- Includes the exception type, message, and inner exception details (if available). + +#### **6. Scoped Variables** + +- Lists all variables currently in scope, including their names and values. +- This information is useful for debugging issues related to variable states. + +#### **7. Runspace Pool Details** + +- Describes the state and configuration of each runspace pool within the Pode application. +- Captures the following details for each runspace pool: + - **Pool Name**: The name assigned to the runspace pool. + - **State**: The current state of the pool (e.g., `Opened`, `Closing`). + - **Result**: The result of the last operation performed on the pool. + - **Instance ID**: A unique identifier for the runspace pool instance. + - **Is Disposed**: Indicates whether the pool has been disposed. + - **Runspace Pool State Info**: Detailed state information about the runspace pool. + - **Initial Session State**: The initial session state used to configure the pool. + - **Cleanup Interval**: The interval used to clean up the pool. + - **Availability**: The availability of the runspace pool for handling requests. + - **Thread Options**: The threading options configured for the pool. + +#### **8. Runspace Details** + +- Provides detailed information about each individual runspace within the application. +- Captures the following information for each runspace: + - **Runspace ID**: A unique identifier for the runspace. + - **Name**: The name of the runspace, often reflecting its purpose (e.g., `Pode_Web_Listener_1`). + - **Scoped Variables**: A detailed list of variables scoped to this runspace, captured using the Pode debugger. + - **Initial Session State**: The initial session state of the runspace. + - **Runspace State Info**: Detailed state information about the runspace, such as `Opened`, `Busy`, or `Disconnected`. ### Dumping Options and Best Practices @@ -337,4 +395,5 @@ In this setup, if an error occurs in the route, `Invoke-PodeDump` is called, cap - **Setting the Path**: Use a dedicated folder for dump files (e.g., `./Dump`) to keep diagnostic files organized. The default path in the configuration can be overridden at runtime if needed. With these configurations and usage practices, the memory dump feature in Pode can provide a powerful tool for diagnostics and debugging, capturing critical state information at the time of failure. - \ No newline at end of file + + diff --git a/docs/Getting-Started/FirstApp.md b/docs/Getting-Started/FirstApp.md index c1e52ceab..46f045566 100644 --- a/docs/Getting-Started/FirstApp.md +++ b/docs/Getting-Started/FirstApp.md @@ -13,7 +13,7 @@ The following steps will run you through creating your first Pode app, and give * Run `pode init` in the console, this will create a basic `package.json` file for you - see the [`CLI`](../CLI) reference for more information. - * The `init` action will ask for some input, leave everything as default (just press enter). +* The `init` action will ask for some input, leave everything as default (just press enter). ```powershell PS> pode init diff --git a/src/Embedded/DebuggerHandler.cs b/src/Embedded/DebuggerHandler.cs index 79aa4cded..422481161 100644 --- a/src/Embedded/DebuggerHandler.cs +++ b/src/Embedded/DebuggerHandler.cs @@ -5,105 +5,93 @@ namespace Pode.Embedded { - public class DebuggerHandler + public class DebuggerHandler : IDisposable { // Collection to store variables collected during the debugging session - private static PSDataCollection variables = new PSDataCollection(); + public PSDataCollection Variables { get; private set; } = new PSDataCollection(); // Event handler for the DebuggerStop event - private static EventHandler debuggerStopHandler; + private EventHandler DebuggerStopHandler; // Flag to indicate whether the DebuggerStop event has been triggered - private static bool eventTriggered = false; + public bool IsEventTriggered { get; private set; } = false; // Flag to control whether variables should be collected during the DebuggerStop event - private static bool shouldCollectVariables = true; + private bool CollectVariables = true; - // Method to attach the DebuggerStop event handler to the runspace's debugger - public static void AttachDebugger(Runspace runspace, bool collectVariables = true) + // Runspace object to store the runspace that the debugger is attached to + private Runspace Runspace; + + public DebuggerHandler(Runspace runspace, bool collectVariables = true) { - // Set the collection flag based on the parameter - shouldCollectVariables = collectVariables; + // Set the collection flag and Runspace object + CollectVariables = collectVariables; + Runspace = runspace; // Initialize the event handler with the OnDebuggerStop method - debuggerStopHandler = new EventHandler(OnDebuggerStop); - - // Attach the event handler to the DebuggerStop event of the runspace's debugger - runspace.Debugger.DebuggerStop += debuggerStopHandler; + DebuggerStopHandler = new EventHandler(OnDebuggerStop); + Runspace.Debugger.DebuggerStop += DebuggerStopHandler; } - // Method to detach the DebuggerStop event handler from the runspace's debugger - public static void DetachDebugger(Runspace runspace) + // Method to detach the DebuggerStop event handler from the runspace's debugger, and general clean-up + public void Dispose() { - if (debuggerStopHandler != null) - { - // Remove the event handler to prevent further event handling - runspace.Debugger.DebuggerStop -= debuggerStopHandler; + IsEventTriggered = false; - // Set the handler to null to clean up - debuggerStopHandler = null; + // Remove the event handler to prevent further event handling + if (DebuggerStopHandler != default(EventHandler)) + { + Runspace.Debugger.DebuggerStop -= DebuggerStopHandler; + DebuggerStopHandler = null; } + + // Clean-up variables + Runspace = default(Runspace); + Variables.Clear(); + + // Garbage collection + GC.SuppressFinalize(this); } // Event handler method that gets called when the debugger stops - private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) + private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) { // Set the eventTriggered flag to true - eventTriggered = true; + IsEventTriggered = true; // Cast the sender to a Debugger object var debugger = sender as Debugger; - if (debugger != null) + if (debugger == default(Debugger)) { - // Enable step mode to allow for command execution during the debug stop - debugger.SetDebuggerStepMode(true); - - PSCommand command = new PSCommand(); + return; + } - if (shouldCollectVariables) - { - // Collect variables - command.AddCommand("Get-PodeDumpScopedVariable"); - } - else - { - // Execute a break - command.AddCommand( "while( $PodeContext.Server.Suspended){ Start-sleep 1}"); - } + // Enable step mode to allow for command execution during the debug stop + debugger.SetDebuggerStepMode(true); - // Create a collection to store the command output - PSDataCollection outputCollection = new PSDataCollection(); + // Collect variables or hang the debugger + var command = new PSCommand(); + command.AddCommand(CollectVariables + ? "Get-PodeDumpScopedVariable" + : "while($PodeContext.Server.Suspended) { Start-Sleep -Milliseconds 500 }"); - // Execute the command within the debugger - debugger.ProcessCommand(command, outputCollection); + // Execute the command within the debugger + var outputCollection = new PSDataCollection(); + debugger.ProcessCommand(command, outputCollection); - // Add results to the variables collection if collecting variables - if (shouldCollectVariables) - { - foreach (var output in outputCollection) - { - variables.Add(output); - } - } - else + // Add results to the variables collection if collecting variables + if (CollectVariables) + { + foreach (var output in outputCollection) { - // Ensure the debugger remains ready for further interaction - debugger.SetDebuggerStepMode(true); + Variables.Add(output); } } - } - - - // Method to check if the DebuggerStop event has been triggered - public static bool IsEventTriggered() - { - return eventTriggered; - } - - // Method to retrieve the collected variables - public static PSDataCollection GetVariables() - { - return variables; + else + { + // Ensure the debugger remains ready for further interaction + debugger.SetDebuggerStepMode(true); + } } } } \ No newline at end of file diff --git a/src/Embedded/NewDebuggerHandler.cs b/src/Embedded/NewDebuggerHandler.cs deleted file mode 100644 index 422481161..000000000 --- a/src/Embedded/NewDebuggerHandler.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Collections.ObjectModel; - -namespace Pode.Embedded -{ - public class DebuggerHandler : IDisposable - { - // Collection to store variables collected during the debugging session - public PSDataCollection Variables { get; private set; } = new PSDataCollection(); - - // Event handler for the DebuggerStop event - private EventHandler DebuggerStopHandler; - - // Flag to indicate whether the DebuggerStop event has been triggered - public bool IsEventTriggered { get; private set; } = false; - - // Flag to control whether variables should be collected during the DebuggerStop event - private bool CollectVariables = true; - - // Runspace object to store the runspace that the debugger is attached to - private Runspace Runspace; - - public DebuggerHandler(Runspace runspace, bool collectVariables = true) - { - // Set the collection flag and Runspace object - CollectVariables = collectVariables; - Runspace = runspace; - - // Initialize the event handler with the OnDebuggerStop method - DebuggerStopHandler = new EventHandler(OnDebuggerStop); - Runspace.Debugger.DebuggerStop += DebuggerStopHandler; - } - - // Method to detach the DebuggerStop event handler from the runspace's debugger, and general clean-up - public void Dispose() - { - IsEventTriggered = false; - - // Remove the event handler to prevent further event handling - if (DebuggerStopHandler != default(EventHandler)) - { - Runspace.Debugger.DebuggerStop -= DebuggerStopHandler; - DebuggerStopHandler = null; - } - - // Clean-up variables - Runspace = default(Runspace); - Variables.Clear(); - - // Garbage collection - GC.SuppressFinalize(this); - } - - // Event handler method that gets called when the debugger stops - private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) - { - // Set the eventTriggered flag to true - IsEventTriggered = true; - - // Cast the sender to a Debugger object - var debugger = sender as Debugger; - if (debugger == default(Debugger)) - { - return; - } - - // Enable step mode to allow for command execution during the debug stop - debugger.SetDebuggerStepMode(true); - - // Collect variables or hang the debugger - var command = new PSCommand(); - command.AddCommand(CollectVariables - ? "Get-PodeDumpScopedVariable" - : "while($PodeContext.Server.Suspended) { Start-Sleep -Milliseconds 500 }"); - - // Execute the command within the debugger - var outputCollection = new PSDataCollection(); - debugger.ProcessCommand(command, outputCollection); - - // Add results to the variables collection if collecting variables - if (CollectVariables) - { - foreach (var output in outputCollection) - { - Variables.Add(output); - } - } - else - { - // Ensure the debugger remains ready for further interaction - debugger.SetDebuggerStepMode(true); - } - } - } -} \ No newline at end of file diff --git a/src/Embedded/OldDebuggerHandler.cs b/src/Embedded/OldDebuggerHandler.cs deleted file mode 100644 index 5db73784b..000000000 --- a/src/Embedded/OldDebuggerHandler.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Collections.ObjectModel; - -namespace Pode.Embedded -{ - public static class DebuggerHandler - { - // Collection to store variables collected during the debugging session - private static PSDataCollection variables = new PSDataCollection(); - - // Event handler for the DebuggerStop event - private static EventHandler debuggerStopHandler; - - // Flag to indicate whether the DebuggerStop event has been triggered - private static bool eventTriggered = false; - - // Flag to control whether variables should be collected during the DebuggerStop event - private static bool shouldCollectVariables = true; - - // Method to attach the DebuggerStop event handler to the runspace's debugger - public static void AttachDebugger(Runspace runspace, bool collectVariables = true) - { - // Set the collection flag based on the parameter - shouldCollectVariables = collectVariables; - - // Initialize the event handler with the OnDebuggerStop method - debuggerStopHandler = new EventHandler(OnDebuggerStop); - - // Attach the event handler to the DebuggerStop event of the runspace's debugger - runspace.Debugger.DebuggerStop += debuggerStopHandler; - } - - // Method to detach the DebuggerStop event handler from the runspace's debugger - public static void DetachDebugger(Runspace runspace) - { - if (debuggerStopHandler != null) - { - // Remove the event handler to prevent further event handling - runspace.Debugger.DebuggerStop -= debuggerStopHandler; - - // Set the handler to null to clean up - debuggerStopHandler = null; - } - } - - // Event handler method that gets called when the debugger stops - private static void OnDebuggerStop(object sender, DebuggerStopEventArgs args) - { - // Set the eventTriggered flag to true - eventTriggered = true; - - // Cast the sender to a Debugger object - var debugger = sender as Debugger; - if (debugger != null) - { - // Enable step mode to allow for command execution during the debug stop - debugger.SetDebuggerStepMode(true); - - PSCommand command = new PSCommand(); - - if (shouldCollectVariables) - { - // Collect variables - command.AddCommand("Get-PodeDumpScopedVariable"); - } - else - { - // Execute a break - command.AddCommand( "while( $PodeContext.Server.Suspended){ Start-sleep 1}"); - } - - // Create a collection to store the command output - PSDataCollection outputCollection = new PSDataCollection(); - - // Execute the command within the debugger - debugger.ProcessCommand(command, outputCollection); - - // Add results to the variables collection if collecting variables - if (shouldCollectVariables) - { - foreach (var output in outputCollection) - { - variables.Add(output); - } - } - else - { - // Ensure the debugger remains ready for further interaction - debugger.SetDebuggerStepMode(true); - } - } - } - - - // Method to check if the DebuggerStop event has been triggered - public static bool IsEventTriggered() - { - return eventTriggered; - } - - // Method to retrieve the collected variables - public static PSDataCollection GetVariables() - { - return variables; - } - } -} \ No newline at end of file diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index cafc568e9..6a42e5883 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -1,13 +1,11 @@ <# .SYNOPSIS - Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. + Captures a memory dump with runspace and exception details when a fatal exception occurs. .DESCRIPTION The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within - the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process - after saving the dump. - + the current directory. If the folder does not exist, it will be created. .PARAMETER ErrorRecord The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. Accepts input from the pipeline. @@ -15,8 +13,6 @@ .PARAMETER Format Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. -.PARAMETER Halt - Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. .PARAMETER Path Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. @@ -24,29 +20,17 @@ .PARAMETER MaxDepth Specifies the maximum depth to traverse when collecting information. -.EXAMPLE - try { - # Simulate a critical error - throw [System.OutOfMemoryException] "Simulated out of memory error" - } - catch { - # Capture the dump in JSON format and halt the application - $_ | Invoke-PodeDump -Format 'json' -Halt - } - - This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. - .EXAMPLE try { # Simulate a critical error throw [System.AccessViolationException] "Simulated access violation error" } catch { - # Capture the dump in YAML format without halting + # Capture the dump in YAML $_ | Invoke-PodeDump -Format 'yaml' } - This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. + This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format. .NOTES This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. @@ -284,7 +268,7 @@ function Invoke-PodeDumpInternal { Pode #> -function Get-PodeRunspaceVariablesViaDebuggerNew { +function Get-PodeRunspaceVariablesViaDebugger { param ( [Parameter(Mandatory)] [System.Management.Automation.Runspaces.Runspace]$Runspace, @@ -339,63 +323,6 @@ function Get-PodeRunspaceVariablesViaDebuggerNew { } -function Get-PodeRunspaceVariablesViaDebugger { - param ( - [Parameter(Mandatory)] - [System.Management.Automation.Runspaces.Runspace]$Runspace, - - [Parameter()] - [int]$Timeout = 60 - ) - - # Initialize variables collection - $variables = @() - try { - - # Attach the debugger using the embedded C# method - [Pode.Embedded.DebuggerHandler]::AttachDebugger($Runspace, $true) - - # Enable debugging and break all - Enable-RunspaceDebug -BreakAll -Runspace $Runspace - - Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine - - # Initialize the timer - $startTime = [DateTime]::UtcNow - - # Wait for the event to be triggered or timeout - while (! [Pode.Embedded.DebuggerHandler]::IsEventTriggered()) { - Start-Sleep -Milliseconds 1000 - Write-PodeHost '.' -NoNewLine - - if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { - Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" - return @{} - } - } - - Write-PodeHost 'Done' - - # Retrieve and output the collected variables from the embedded C# code - $variables = [Pode.Embedded.DebuggerHandler]::GetVariables() - } - catch { - # Log the error details using Write-PodeErrorLog. - # This ensures that any exceptions thrown during the execution are logged appropriately. - $_ | Write-PodeErrorLog - } - finally { - # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. - [Pode.Embedded.DebuggerHandler]::DetachDebugger($Runspace) - - # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. - Disable-RunspaceDebug -Runspace $Runspace - } - - return $variables[0] -} - - <# .SYNOPSIS Collects and serializes variables from different scopes (Local, Script, Global). diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 53aac4379..6e6963fce 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2622,7 +2622,7 @@ function Get-PodeRelativePath { $RootPath = $PodeContext.Server.Root } - $Path = [System.IO.Path]::Combine($RootPath, $Path.Substring(2)) + $Path = [System.IO.Path]::Combine($RootPath, ($Path -replace '^\.{1,2}([\\\/])?', '')) } # if flagged, resolve the path diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 815de45bb..6de2d2fe1 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -248,10 +248,6 @@ function Start-PodeServer { if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { Clear-PodeKeyPressed Invoke-PodeDumpInternal -Format $PodeContext.Server.Debug.Dump.Format -Path $PodeContext.Server.Debug.Dump.Path -MaxDepth $PodeContext.Server.Debug.Dump.MaxDepth - if ($PodeContext.Server.Debug.Dump.Param.Halt) { - Write-PodeHost -ForegroundColor Red 'Halt switch detected. Closing the application.' - break - } } if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index e19c9243d..9e6171bcd 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1495,13 +1495,12 @@ function Invoke-PodeGC { <# .SYNOPSIS - Captures a memory dump with runspace and exception details when a fatal exception occurs, with an optional halt switch to close the application. + Captures a memory dump with runspace and exception details when a fatal exception occurs. .DESCRIPTION The Invoke-PodeDump function gathers diagnostic information, including process memory usage, exception details, runspace information, and variables from active runspaces. It saves this data in the specified format (JSON, CLIXML, Plain Text, Binary, or YAML) in a "Dump" folder within - the current directory. If the folder does not exist, it will be created. An optional `-Halt` switch is available to terminate the PowerShell process - after saving the dump. + the current directory. If the folder does not exist, it will be created. .PARAMETER ErrorRecord The ErrorRecord object representing the fatal exception that triggered the memory dump. This provides details on the error, such as message and stack trace. @@ -1510,8 +1509,6 @@ function Invoke-PodeGC { .PARAMETER Format Specifies the format for saving the dump file. Supported formats are 'json', 'clixml', 'txt', 'bin', and 'yaml'. -.PARAMETER Halt - Switch to specify whether to terminate the application after saving the memory dump. If set, the function will close the PowerShell process. .PARAMETER Path Specifies the directory where the dump file will be saved. If the directory does not exist, it will be created. Defaults to a "Dump" folder. @@ -1519,17 +1516,6 @@ function Invoke-PodeGC { .PARAMETER MaxDepth Specifies the maximum depth of objects to serialize when saving the dump in JSON or YAML -.EXAMPLE - try { - # Simulate a critical error - throw [System.OutOfMemoryException] "Simulated out of memory error" - } - catch { - # Capture the dump in JSON format and halt the application - $_ | Invoke-PodeDump -Format 'json' -Halt - } - - This example catches a simulated OutOfMemoryException and pipes it to Invoke-PodeDump to capture the error in JSON format and halt the application. .EXAMPLE try { @@ -1537,11 +1523,11 @@ function Invoke-PodeGC { throw [System.AccessViolationException] "Simulated access violation error" } catch { - # Capture the dump in YAML format without halting + # Capture the dump in YAML format $_ | Invoke-PodeDump -Format 'yaml' } - This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format without halting the application. + This example catches a simulated AccessViolationException and pipes it to Invoke-PodeDump to capture the error in YAML format. .NOTES This function is designed to assist with post-mortem analysis by capturing critical application state information when a fatal error occurs. @@ -1562,9 +1548,6 @@ function Invoke-PodeDump { [string] $Path, - [switch] - $Halt, - [int] $MaxDepth ) diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index 4c67439b8..177204213 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1058,7 +1058,7 @@ Describe 'Get-PodeRelativePath' { } It 'Returns path for a relative path joined to default root' { - Get-PodeRelativePath -Path './path' -JoinRoot | Should -Be 'c:/./path' + Get-PodeRelativePath -Path './path' -JoinRoot | Should -Be 'c:/path' } It 'Returns resolved path for a relative path joined to default root when resolving' { @@ -1072,7 +1072,7 @@ Describe 'Get-PodeRelativePath' { } It 'Returns path for a relative path joined to passed root' { - Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should -Be 'e:/./path' + Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should -Be 'e:/path' } It 'Throws error for path ot existing' { From 79fdf464594759c40c449f3b98505d34fb97d452 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 1 Dec 2024 10:46:36 -0800 Subject: [PATCH 21/34] suspend http and tcp --- examples/Web-Dump.ps1 | 23 +++++-- src/Private/Dump.ps1 | 126 ++++++++++++++++++++++++++++++++++++-- src/Private/Schedules.ps1 | 13 +++- src/Private/Server.ps1 | 2 +- src/Public/Core.ps1 | 8 ++- 5 files changed, 154 insertions(+), 18 deletions(-) diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 index 9ce2c258a..c2ddd3dd4 100644 --- a/examples/Web-Dump.ps1 +++ b/examples/Web-Dump.ps1 @@ -43,13 +43,13 @@ Start-PodeServer -Threads 4 -ScriptBlock { # listen on localhost:8081 Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http - Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Https -SelfSigned Add-PodeEndpoint -Address localhost -Port 8083 -Protocol Http - Add-PodeEndpoint -Address localhost -Port 8025 -Protocol Smtp - Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -Name 'WS1' - Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'WS' + #Add-PodeEndpoint -Address localhost -Port 8025 -Protocol Smtp + # Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -Name 'WS1' + # Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'WS' Add-PodeEndpoint -Address localhost -Port 8100 -Protocol Tcp - + Add-PodeEndpoint -Address localhost -Port 9002 -Protocol Tcps -SelfSigned # set view engine to pode renderer Set-PodeViewEngine -Type Html @@ -102,7 +102,7 @@ Start-PodeServer -Threads 4 -ScriptBlock { } # setup an smtp handler - Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { + <# Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { Write-PodeHost '- - - - - - - - - - - - - - - - - -' Write-PodeHost $SmtpEvent.Email.From Write-PodeHost $SmtpEvent.Email.To @@ -136,5 +136,16 @@ Start-PodeServer -Threads 4 -ScriptBlock { } Send-PodeSignal -Value @{ message = $msg } + }#> + + Add-PodeVerb -Verb 'QUIT' -ScriptBlock { + Write-PodeTcpClient -Message 'Bye!' + Close-PodeTcpClient + } + + Add-PodeVerb -Verb 'HELLO3' -ScriptBlock { + Write-PodeTcpClient -Message "Hi! What's your name?" + $name = Read-PodeTcpClient -CRLFMessageEnd + Write-PodeTcpClient -Message "Hi, $($name)!" } } \ No newline at end of file diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 6a42e5883..63fce3b8c 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -154,7 +154,9 @@ function Invoke-PodeDumpInternal { $runspacePoolDetails = @() # Retrieve all runspaces related to Pode ordered by name - $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' } | Sort-Object Name + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` + $_.Name -notlike '*__pode_session_inmem_cleanup__*' } | Sort-Object Name + $runspaceDetails = @{} foreach ($r in $runspaces) { @@ -263,10 +265,6 @@ function Invoke-PodeDumpInternal { .NOTES The function uses an embedded C# class to handle the `DebuggerStop` event. This class attaches and detaches the debugger and processes commands in the stopped state. The collected variables are returned as a `PSObject`. - -.COMPONENT - Pode - #> function Get-PodeRunspaceVariablesViaDebugger { param ( @@ -292,7 +290,58 @@ function Get-PodeRunspaceVariablesViaDebugger { while (!$debugger.IsEventTriggered) { Start-Sleep -Milliseconds 500 Write-PodeHost '.' -NoNewLine + if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge 1) { + if ($Runspace.Name.StartsWith('Pode_Web_Listener')) { + $uris = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and !$_.Url.StartsWith('https') }).Url + if ($uris) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + try { + Invoke-WebRequest -Uri $uris -ErrorAction SilentlyContinue > $null 2>&1 + } + catch { + # Suppress any exceptions + Write-Verbose -Message $_ + } + } + $uris = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and $_.Url.StartsWith('https') }).Url + if ($uris) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + try { + Invoke-WebRequest -Uri $uris -SkipCertificateCheck -ErrorAction SilentlyContinue > $null 2>&1 + } + catch { + # Suppress any exceptions + Write-Verbose -Message $_ + } + } + + } + elseif ($Runspace.Name.StartsWith('Pode_Smtp_Listener')) { + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Smtp' }).Url + + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + Send-PodeTelnetCommand -ServerUri $uri -command "HELO domain.com" + } + }elseif ($Runspace.Name.StartsWith('Pode_Tcp_Listener')) { + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Tcp' }).Url + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + Send-PodeTelnetCommand -ServerUri $uri -command 'aaa' + } + + } + + } if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" return @{} @@ -478,4 +527,69 @@ function Initialize-PodeDebugHandler { # Embed C# code to handle the DebuggerStop event Add-Type -LiteralPath ([System.IO.Path]::Combine((Get-PodeModuleRootPath), 'Embedded', 'DebuggerHandler.cs')) -ErrorAction Stop } -} \ No newline at end of file +} + +function Send-PodeTelnetCommand { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$ServerUri, # Server URI in the format tcp://hostname:port or tcps://hostname:port + + [Parameter(Mandatory = $true)] + [string]$Command, # The string command to send (e.g., "HELO domain.com") + + [int]$Timeout = 2000 # Timeout in milliseconds + ) + + try { + # Parse the ServerUri to extract the host and port + $uri = [System.Uri]::new($ServerUri) + if ($uri.Scheme -notin @("tcp", "tcps")) { + throw "Invalid URI scheme. Expected 'tcp://' or 'tcps://', but got '$($uri.Scheme)://'." + } + $server = $uri.Host + $port = $uri.Port + + # Create a TCP client and connect to the server + $tcpClient = [System.Net.Sockets.TcpClient]::new() + $tcpClient.Connect($server, $port) + $tcpClient.ReceiveTimeout = $Timeout + $tcpClient.SendTimeout = $Timeout + + # Get the network stream + $stream = $tcpClient.GetStream() + + # Wrap the stream in SslStream for TCPS connections + if ($uri.Scheme -eq "tcps") { + $sslStream = [System.Net.Security.SslStream]::new($stream, $false, { $true }) # Simple certificate validation + $sslStream.AuthenticateAsClient($server) + $stream = $sslStream + } + + # Create a stream writer and reader for the connection + $writer = [System.IO.StreamWriter]::new($stream) + $reader = [System.IO.StreamReader]::new($stream) + + # Send the command + Write-Verbose "Sending command: $Command" + $writer.WriteLine($Command) + $writer.Flush() + + # Read the response + $response = @() + while ($reader.Peek() -ge 0) { + $response += $reader.ReadLine() + } + + # Close the connection + $writer.Close() + $reader.Close() + $stream.Close() + $tcpClient.Close() + + # Return the response + return $response -join "`n" + } catch { + Write-Verbose "An error occurred: $_" + } +} diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index 924a888d2..decdc8bf9 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -88,9 +88,16 @@ function Start-PodeScheduleRunspace { # complete any schedules Complete-PodeInternalSchedule -Now $_now - - # cron expression only goes down to the minute, so sleep for 1min - Start-Sleep -Seconds (60 - [DateTime]::Now.Second) + + # Calculate the remaining seconds to sleep until the next minute + $remainingSeconds = 60 - [DateTime]::Now.Second + + # Loop in 5-second intervals until the remaining seconds are covered + while ($remainingSeconds -gt 0) { + $sleepTime = [math]::Min(5, $remainingSeconds) # Sleep for 5 seconds or remaining time + Start-Sleep -Seconds $sleepTime + $remainingSeconds -= $sleepTime + } } catch { $_ | Write-PodeErrorLog diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 67d8e20fe..361175569 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -595,7 +595,7 @@ function Resume-PodeServerInternal { $PodeContext.Server.Suspended = $false # Pause briefly to ensure any required internal processes have time to stabilize - Start-Sleep 5 + Start-Sleep -Seconds 5 # Retrieve all runspaces related to Pode $runspaces = Get-Runspace -name 'Pode_*' diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 6de2d2fe1..e777c2157 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -1218,8 +1218,12 @@ function Add-PodeEndpoint { } # set the url of this endpoint - $obj.Url = "$($obj.Protocol)://$($obj.FriendlyName):$($obj.Port)/" - + if (($obj.Protocol -eq 'http') -or ($obj.Protocol -eq 'https')) { + $obj.Url = "$($obj.Protocol)://$($obj.FriendlyName):$($obj.Port)/" + } + else { + $obj.Url = "$($obj.Protocol)://$($obj.FriendlyName):$($obj.Port)" + } # if the address is non-local, then check admin privileges if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and !(Test-PodeIsAdminUser)) { # Must be running with administrator privileges to listen on non-localhost addresses From 900001a5c52b01171d101006ba3dafa77de5e510 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 1 Dec 2024 18:25:45 -0800 Subject: [PATCH 22/34] fix tests --- src/Private/Dump.ps1 | 11 +++---- src/Private/Helpers.ps1 | 56 ++++++++++++++++++++++++++++++++++-- tests/unit/Helpers.Tests.ps1 | 12 ++++++-- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 63fce3b8c..348c7b561 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -100,7 +100,7 @@ function Invoke-PodeDumpInternal { # Process block to handle each pipeline input process { # Ensure Dump directory exists in the specified path - $Path = Get-PodeRelativePath -Path $Path -JoinRoot + $Path = Get-PodeRelativePath -Path $Path -JoinRoot -NormalizeRelativePath if (!(Test-Path -Path $Path)) { New-Item -ItemType Directory -Path $Path | Out-Null @@ -327,7 +327,7 @@ function Get-PodeRunspaceVariablesViaDebugger { if ( $uri -is [array]) { $uri = $uri[0] } - Send-PodeTelnetCommand -ServerUri $uri -command "HELO domain.com" + Send-PodeTelnetCommand -ServerUri $uri -command "HELO domain.com`n" } }elseif ($Runspace.Name.StartsWith('Pode_Tcp_Listener')) { $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Tcp' }).Url @@ -335,7 +335,7 @@ function Get-PodeRunspaceVariablesViaDebugger { if ( $uri -is [array]) { $uri = $uri[0] } - Send-PodeTelnetCommand -ServerUri $uri -command 'aaa' + Send-PodeTelnetCommand -ServerUri $uri -command "`n" } } @@ -528,7 +528,7 @@ function Initialize-PodeDebugHandler { Add-Type -LiteralPath ([System.IO.Path]::Combine((Get-PodeModuleRootPath), 'Embedded', 'DebuggerHandler.cs')) -ErrorAction Stop } } - + function Send-PodeTelnetCommand { [CmdletBinding()] param ( @@ -545,7 +545,8 @@ function Send-PodeTelnetCommand { # Parse the ServerUri to extract the host and port $uri = [System.Uri]::new($ServerUri) if ($uri.Scheme -notin @("tcp", "tcps")) { - throw "Invalid URI scheme. Expected 'tcp://' or 'tcps://', but got '$($uri.Scheme)://'." + Write-Verbose "Invalid URI scheme. Expected 'tcp://' or 'tcps://', but got '$($uri.Scheme)://'." + return } $server = $uri.Host $port = $uri.Port diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 6e6963fce..bba2dcb00 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2595,7 +2595,50 @@ function Find-PodeFileForContentType { # no file was found return $null } +<# +.SYNOPSIS + Resolves and processes a relative or absolute file system path based on the specified parameters. + +.DESCRIPTION + This function processes a given path and applies various transformations and checks based on the provided parameters. It supports resolving relative paths, joining them with a root path, normalizing relative paths, and verifying path existence. + +.PARAMETER Path + The file system path to be processed. This can be relative or absolute. + +.PARAMETER RootPath + (Optional) The root path to join with if the provided path is relative and the -JoinRoot switch is enabled. + +.PARAMETER JoinRoot + Indicates that the relative path should be joined to the specified root path. If no RootPath is provided, the Pode context server root will be used. + +.PARAMETER Resolve + Resolves the path to its absolute, full path. + +.PARAMETER TestPath + Verifies if the resolved path exists. Throws an exception if the path does not exist. + +.PARAMETER NormalizeRelativePath + (Optional) Removes any leading './' or '../' segments from the relative path when used with -JoinRoot. This ensures that the path is normalized before being joined with the root path. + +.OUTPUTS + System.String + Returns the resolved and processed path as a string. +.EXAMPLE + # Example 1: Resolve a relative path and join it with a root path + Get-PodeRelativePath -Path './example' -RootPath 'C:\Root' -JoinRoot + +.EXAMPLE + # Example 2: Resolve and normalize a relative path + Get-PodeRelativePath -Path '../example' -RootPath 'C:\Root' -JoinRoot -NormalizeRelativePath + +.EXAMPLE + # Example 3: Test if a path exists + Get-PodeRelativePath -Path 'C:\Root\example.txt' -TestPath + +.NOTES + This is an internal function and may change in future releases of Pode +#> function Get-PodeRelativePath { param( [Parameter(Mandatory = $true)] @@ -2613,7 +2656,11 @@ function Get-PodeRelativePath { $Resolve, [switch] - $TestPath + $TestPath, + + [switch] + $NormalizeRelativePath + ) # if the path is relative, join to root if flagged @@ -2622,7 +2669,12 @@ function Get-PodeRelativePath { $RootPath = $PodeContext.Server.Root } - $Path = [System.IO.Path]::Combine($RootPath, ($Path -replace '^\.{1,2}([\\\/])?', '')) + if ($NormalizeRelativePath) { + $Path = [System.IO.Path]::Combine($RootPath, ($Path -replace '^\.{1,2}([\\\/])?', '')) + } + else { + $Path = [System.IO.Path]::Combine($RootPath, $Path) + } } # if flagged, resolve the path diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index 177204213..03e377eea 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1058,7 +1058,11 @@ Describe 'Get-PodeRelativePath' { } It 'Returns path for a relative path joined to default root' { - Get-PodeRelativePath -Path './path' -JoinRoot | Should -Be 'c:/path' + Get-PodeRelativePath -Path './path' -JoinRoot | Should -Be 'c:/./path' + } + + It 'Returns path for a relative path joined to default root normalized' { + Get-PodeRelativePath -Path './path' -JoinRoot -NormalizeRelativePath | Should -Be 'c:/path' } It 'Returns resolved path for a relative path joined to default root when resolving' { @@ -1072,7 +1076,11 @@ Describe 'Get-PodeRelativePath' { } It 'Returns path for a relative path joined to passed root' { - Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should -Be 'e:/path' + Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should -Be 'e:/./path' + } + + It 'Returns path for a relative path joined to passed root normalized' { + Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' -NormalizeRelativePath | Should -Be 'e:/path' } It 'Throws error for path ot existing' { From 526325c54ded2b693468a76eef8ca690fb6e7225 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 1 Dec 2024 19:47:18 -0800 Subject: [PATCH 23/34] WSS suspension --- examples/Web-Dump.ps1 | 10 +- src/Private/Dump.ps1 | 224 +++++++++++++++++++++++++++++++----------- 2 files changed, 173 insertions(+), 61 deletions(-) diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 index c2ddd3dd4..6fc3cf9ef 100644 --- a/examples/Web-Dump.ps1 +++ b/examples/Web-Dump.ps1 @@ -46,8 +46,8 @@ Start-PodeServer -Threads 4 -ScriptBlock { Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Https -SelfSigned Add-PodeEndpoint -Address localhost -Port 8083 -Protocol Http #Add-PodeEndpoint -Address localhost -Port 8025 -Protocol Smtp - # Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -Name 'WS1' - # Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'WS' + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -Name 'WS1' + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'WS' Add-PodeEndpoint -Address localhost -Port 8100 -Protocol Tcp Add-PodeEndpoint -Address localhost -Port 9002 -Protocol Tcps -SelfSigned @@ -102,7 +102,7 @@ Start-PodeServer -Threads 4 -ScriptBlock { } # setup an smtp handler - <# Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { + <# Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { Write-PodeHost '- - - - - - - - - - - - - - - - - -' Write-PodeHost $SmtpEvent.Email.From Write-PodeHost $SmtpEvent.Email.To @@ -121,7 +121,7 @@ Start-PodeServer -Threads 4 -ScriptBlock { $SmtpEvent.Email.Headers | out-default Write-PodeHost '- - - - - - - - - - - - - - - - - -' } - +#> # GET request for web page Add-PodeRoute -Method Get -Path '/' -EndpointName 'WS' -ScriptBlock { Write-PodeViewResponse -Path 'websockets' @@ -136,7 +136,7 @@ Start-PodeServer -Threads 4 -ScriptBlock { } Send-PodeSignal -Value @{ message = $msg } - }#> + } Add-PodeVerb -Verb 'QUIT' -ScriptBlock { Write-PodeTcpClient -Message 'Bye!' diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 348c7b561..dfa56fa18 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -44,7 +44,7 @@ function Invoke-PodeDumpInternal { $ErrorRecord, [Parameter()] - [ValidateSet('json', 'clixml', 'txt', 'bin', 'yaml')] + [ValidateSet('JSON', 'CLIXML', 'TXT', 'BIN', 'YAML')] [string] $Format, @@ -208,7 +208,7 @@ function Invoke-PodeDumpInternal { } $dumpFilePath = Join-Path -Path $Path -ChildPath "PowerShellDump_$(Get-Date -Format 'yyyyMMdd_HHmmss').$($Format.ToLower())" # Determine file extension and save format based on selected Format - switch ($Format) { + switch ($Format.ToLower()) { 'json' { $dumpInfo | ConvertTo-Json -Depth $MaxDepth -WarningAction SilentlyContinue | Out-File -FilePath $dumpFilePath break @@ -286,61 +286,22 @@ function Get-PodeRunspaceVariablesViaDebugger { # Wait for the event to be triggered or timeout $startTime = [DateTime]::UtcNow Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine + Start-Sleep -Milliseconds 1000 + Write-PodeHost '..' -NoNewLine - while (!$debugger.IsEventTriggered) { - Start-Sleep -Milliseconds 500 - Write-PodeHost '.' -NoNewLine - if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge 1) { - if ($Runspace.Name.StartsWith('Pode_Web_Listener')) { - $uris = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and !$_.Url.StartsWith('https') }).Url - if ($uris) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - try { - Invoke-WebRequest -Uri $uris -ErrorAction SilentlyContinue > $null 2>&1 - } - catch { - # Suppress any exceptions - Write-Verbose -Message $_ - } - } - $uris = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and $_.Url.StartsWith('https') }).Url - if ($uris) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - try { - Invoke-WebRequest -Uri $uris -SkipCertificateCheck -ErrorAction SilentlyContinue > $null 2>&1 - } - catch { - # Suppress any exceptions - Write-Verbose -Message $_ - } - } + Send-PodeInterrupt -Name $Runspace.Name - } - elseif ($Runspace.Name.StartsWith('Pode_Smtp_Listener')) { - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Smtp' }).Url - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - Send-PodeTelnetCommand -ServerUri $uri -command "HELO domain.com`n" - } - }elseif ($Runspace.Name.StartsWith('Pode_Tcp_Listener')) { - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Tcp' }).Url - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - Send-PodeTelnetCommand -ServerUri $uri -command "`n" - } - } + Write-PodeHost '.' -NoNewLine + while (!$debugger.IsEventTriggered) { + Start-Sleep -Milliseconds 500 + Write-PodeHost '.' -NoNewLine + + if (([DateTime]::UtcNow - $startTime).TotalSeconds % 5 -eq 0) { + Send-PodeInterrupt -Name $Runspace.Name } if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" @@ -533,10 +494,10 @@ function Send-PodeTelnetCommand { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [string]$ServerUri, # Server URI in the format tcp://hostname:port or tcps://hostname:port + [string]$ServerUri, # Server URI in the format tcp://hostname:port or tcps://hostname:port [Parameter(Mandatory = $true)] - [string]$Command, # The string command to send (e.g., "HELO domain.com") + [string]$Command, # The string command to send (e.g., "HELO domain.com") [int]$Timeout = 2000 # Timeout in milliseconds ) @@ -544,7 +505,7 @@ function Send-PodeTelnetCommand { try { # Parse the ServerUri to extract the host and port $uri = [System.Uri]::new($ServerUri) - if ($uri.Scheme -notin @("tcp", "tcps")) { + if ($uri.Scheme -notin @('tcp', 'tcps')) { Write-Verbose "Invalid URI scheme. Expected 'tcp://' or 'tcps://', but got '$($uri.Scheme)://'." return } @@ -561,7 +522,7 @@ function Send-PodeTelnetCommand { $stream = $tcpClient.GetStream() # Wrap the stream in SslStream for TCPS connections - if ($uri.Scheme -eq "tcps") { + if ($uri.Scheme -eq 'tcps') { $sslStream = [System.Net.Security.SslStream]::new($stream, $false, { $true }) # Simple certificate validation $sslStream.AuthenticateAsClient($server) $stream = $sslStream @@ -590,7 +551,158 @@ function Send-PodeTelnetCommand { # Return the response return $response -join "`n" - } catch { + } + catch { Write-Verbose "An error occurred: $_" } } + + + +function Send-PodeWebSocketMessage { + param ( + [Parameter(Mandatory = $true)] + [string] + $Message, + + [Parameter(Mandatory = $true)] + [string] + $ServerUri + ) + + # Load the WebSocket Client + # Add-Type -AssemblyName System.Net.Http + + # Initialize the WebSocket client + $clientWebSocket = [System.Net.WebSockets.ClientWebSocket]::new() + + try { + # Connect to the WebSocket server + Write-Verbose "Connecting to WebSocket server at $ServerUri..." + $clientWebSocket.ConnectAsync([uri]::new($ServerUri), [System.Threading.CancellationToken]::None).Wait() + Write-Verbose 'Connected to WebSocket server.' + + # Convert the message to bytes + $buffer = [System.Text.Encoding]::UTF8.GetBytes($Message) + $segment = [System.ArraySegment[byte]]::new($buffer) + + # Send the message + Write-Verbose "Sending message: $Message" + $clientWebSocket.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).Wait() + Write-Verbose 'Message sent successfully.' + + # Optional: Receive a response (if expected) + $responseBuffer = [byte[]]::new( 1024) + $responseSegment = [System.ArraySegment[byte]]::new($responseBuffer) + $result = $clientWebSocket.ReceiveAsync($responseSegment, [System.Threading.CancellationToken]::None).Result + + # Decode and display the response message + $responseMessage = [System.Text.Encoding]::UTF8.GetString($responseBuffer, 0, $result.Count) + Write-Verbose "Received response: $responseMessage" + + } + catch { + Write-Error "An error occurred: $_" + } + finally { + # Close the WebSocket connection + try { + $clientWebSocket.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Closing', [System.Threading.CancellationToken]::None).Wait() + Write-Verbose 'WebSocket connection closed.' + } + catch { + Write-Verbose "Failed to close WebSocket connection: $_" + } + + # Dispose of the WebSocket client + $clientWebSocket.Dispose() + } +} + +function Send-PodeInterrupt { + param ( + [string] + $Name + ) + + if ($Name.StartsWith('Pode_Web_Listener') ) { + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and !$_.Url.StartsWith('https') }).Url + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + try { + Invoke-WebRequest -Uri $uri -ErrorAction SilentlyContinue > $null 2>&1 + } + catch { + # Suppress any exceptions + Write-Verbose -Message $_ + } + } + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and $_.Url.StartsWith('https') }).Url + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + try { + Invoke-WebRequest -Uri $uri -SkipCertificateCheck -ErrorAction SilentlyContinue > $null 2>&1 + } + catch { + # Suppress any exceptions + Write-Verbose -Message $_ + } + } + } + elseif ($Name.StartsWith('Pode_Smtp_Listener')) { + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Smtp' }).Url + + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + Send-PodeTelnetCommand -ServerUri $uri -command "HELO domain.com`n" + } + } + elseif ($Name.StartsWith('Pode_Tcp_Listener')) { + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Tcp' }).Url + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + Send-PodeTelnetCommand -ServerUri $uri -command "`n" + } + + } + elseif ($Name.StartsWith('Pode_Signals_Broadcaster')) { + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Signals' }).Url + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + + Send-PodeWebSocketMessage -ServerUri $uri -Message '{"message":"Broadcast from PowerShell!"}' + } + + } + elseif ( $Name.StartsWith('Pode_Signals_Listener')) { + $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Signals' }).Url + if ($uri) { + if ( $uri -is [array]) { + $uri = $uri[0] + } + # $newuri = 'http' + $uri.Substring(2) #deal with both http and https + try { + # Invoke-WebRequest -Uri $newuri -ErrorAction SilentlyContinue -SkipCertificateCheck > $null 2>&1 + + + Send-PodeWebSocketMessage -ServerUri $uri -Message '{"message":"Broadcast from PowerShell!"}' + } + catch { + # Suppress any exceptions + Write-Verbose -Message $_ + } + } + + } + +} \ No newline at end of file From ce5cc012426517ed23d4ff8cd081ceead467abcf Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 2 Dec 2024 08:31:08 -0800 Subject: [PATCH 24/34] Fix suspend --- src/Private/Dump.ps1 | 132 +++++++++++++++++++++++++++-------------- src/Private/Server.ps1 | 34 ++++------- 2 files changed, 98 insertions(+), 68 deletions(-) diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index dfa56fa18..c53b0dfbc 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -164,7 +164,7 @@ function Invoke-PodeDumpInternal { Id = $r.Id Name = @{ $r.Name = @{ - ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r + ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r -NumberOfRunspaces $runspaces.Count } } InitialSessionState = $r.InitialSessionState @@ -254,6 +254,9 @@ function Invoke-PodeDumpInternal { .PARAMETER Timeout The maximum time (in seconds) to wait for the debugger stop event to be triggered. Defaults to 60 seconds. +.PARAMETER NumberOfRunspaces + The total numebr of Runspaces to collect. + .EXAMPLE $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $runspace.Open() @@ -268,65 +271,56 @@ function Invoke-PodeDumpInternal { #> function Get-PodeRunspaceVariablesViaDebugger { param ( - [Parameter(Mandatory)] - [System.Management.Automation.Runspaces.Runspace]$Runspace, + [Parameter(Mandatory = $true)] + [System.Management.Automation.Runspaces.Runspace] + $Runspace, [Parameter()] - [int]$Timeout = 60 - ) - - # Initialize variables collection - $variables = @() - try { - - # Attach the debugger and break all - $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) - Enable-RunspaceDebug -BreakAll -Runspace $Runspace + [int] + $Timeout = 60, - # Wait for the event to be triggered or timeout - $startTime = [DateTime]::UtcNow - Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine - Start-Sleep -Milliseconds 1000 - Write-PodeHost '..' -NoNewLine + [Parameter(Mandatory = $true)] + [int] + $NumberOfRunspaces - Send-PodeInterrupt -Name $Runspace.Name + ) + # Initialize variables collection + $variables = @(@{}) + for ($i = 0; $i -le 3; $i++) { + try { + # Attach the debugger and break all + $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) + Enable-RunspaceDebug -BreakAll -Runspace $Runspace - Write-PodeHost '.' -NoNewLine + # Wait for the event to be triggered or timeout - while (!$debugger.IsEventTriggered) { - Start-Sleep -Milliseconds 500 - Write-PodeHost '.' -NoNewLine + Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine - if (([DateTime]::UtcNow - $startTime).TotalSeconds % 5 -eq 0) { - Send-PodeInterrupt -Name $Runspace.Name - } - if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { - Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" - return @{} + # Suspend the runspace + if (Suspend-PodeRunspace -Runspace $Runspace) { + # Retrieve and output the collected variables from the embedded C# code + $variables = $debugger.Variables + break } - } - - Write-PodeHost 'Done' - # Retrieve and output the collected variables from the embedded C# code - $variables = $debugger.Variables - } - catch { - # Log the error details using Write-PodeErrorLog. - # This ensures that any exceptions thrown during the execution are logged appropriately. - $_ | Write-PodeErrorLog - } - finally { - # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. - if ($null -ne $debugger) { - $debugger.Dispose() } + catch { + # Log the error details using Write-PodeErrorLog. + # This ensures that any exceptions thrown during the execution are logged appropriately. + $_ | Write-PodeErrorLog + } + finally { + # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. + if ($null -ne $debugger) { + $debugger.Dispose() + } - # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. - Disable-RunspaceDebug -Runspace $Runspace + # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. + Disable-RunspaceDebug -Runspace $Runspace + } } return $variables[0] @@ -704,5 +698,51 @@ function Send-PodeInterrupt { } } + else { + return $false + } + + Return $true +} + + +function Suspend-PodeRunspace { + param ( + $Runspace + ) + # Attach the debugger and break all + $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) + Enable-RunspaceDebug -BreakAll -Runspace $Runspace + + # Wait for the event to be triggered or timeout + $startTime = [DateTime]::UtcNow + Start-Sleep -Milliseconds 1000 + # Write-PodeHost '..' -NoNewLine + + # Send-PodeInterrupt -Name $Runspace.Name + + Write-PodeHost '.' -NoNewLine + + while (!$debugger.IsEventTriggered) { + Start-Sleep -Milliseconds 1000 + + if (([int]([DateTime]::UtcNow - $startTime).TotalSeconds) % 5 -eq 0) { + if (Send-PodeInterrupt -Name $Runspace.Name) { + Write-PodeHost '*' -NoNewLine + } + else { + Write-PodeHost '.' -NoNewLine + } + } + else { + Write-PodeHost '.' -NoNewLine + } + if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { + Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" + return $false + } + } + Write-PodeHost 'Done' + return $true } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 361175569..ce8f68ec2 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -214,7 +214,8 @@ function Show-PodeConsoleInfo { Write-PodeHost "`rPode $(Get-PodeVersion) (PID: $($PID)) [$status] " -ForegroundColor Cyan -Force:$Force -NoNewLine if ($PodeContext.Server.Console.ShowHelp -or $Force) { Write-PodeHost -Force:$Force - }else{ + } + else { Write-PodeHost '- Ctrl+H for the Command List' -ForegroundColor Cyan } } @@ -513,12 +514,13 @@ function Suspend-PodeServerInternal { $PodeContext.Server.Suspended = $true # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) - $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' } | Sort-Object Name + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` + $_.Name -notlike '*__pode_session_inmem_cleanup__*' } | Sort-Object Name foreach ($runspace in $runspaces) { try { # Attach debugger to the runspace - [Pode.Embedded.DebuggerHandler]::AttachDebugger($runspace, $false) + $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) # Enable debugging and pause execution Enable-RunspaceDebug -BreakAll -Runspace $runspace @@ -526,27 +528,14 @@ function Suspend-PodeServerInternal { # Inform user about the suspension process for the current runspace Write-PodeHost "Waiting for $($runspace.Name) to be suspended." -NoNewLine -ForegroundColor Yellow - # Initialize the timer - $startTime = [DateTime]::UtcNow - - # Wait for the suspension event or until timeout - while (! [Pode.Embedded.DebuggerHandler]::IsEventTriggered()) { - Start-Sleep -Milliseconds 1000 - Write-PodeHost '.' -NoNewLine - - # Check for timeout - if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { - Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" -ForegroundColor Red - return - } - } - - # Inform user that the suspension is complete - Write-PodeHost 'Done' -ForegroundColor Green + # Suspend the runspace + Suspend-PodeRunspace -Runspace $Runspace } finally { - # Detach the debugger from the runspace - [Pode.Embedded.DebuggerHandler]::DetachDebugger($runspace) + # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. + if ($null -ne $debugger) { + $debugger.Dispose() + } } } @@ -562,6 +551,7 @@ function Suspend-PodeServerInternal { } finally { Reset-PodeCancellationToken -Type SuspendResume + } } From 9cdaf4e299c681ee7ebdd5785bae621e41d9ee8a Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 2 Dec 2024 08:48:34 -0800 Subject: [PATCH 25/34] Update DebuggerHandler.cs --- src/Embedded/DebuggerHandler.cs | 73 +++++++++++++++++---------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/Embedded/DebuggerHandler.cs b/src/Embedded/DebuggerHandler.cs index 422481161..96257e12a 100644 --- a/src/Embedded/DebuggerHandler.cs +++ b/src/Embedded/DebuggerHandler.cs @@ -8,70 +8,76 @@ namespace Pode.Embedded public class DebuggerHandler : IDisposable { // Collection to store variables collected during the debugging session - public PSDataCollection Variables { get; private set; } = new PSDataCollection(); + public PSDataCollection Variables { get; private set; } // Event handler for the DebuggerStop event - private EventHandler DebuggerStopHandler; + private EventHandler _debuggerStopHandler; // Flag to indicate whether the DebuggerStop event has been triggered - public bool IsEventTriggered { get; private set; } = false; + public bool IsEventTriggered { get; private set; } // Flag to control whether variables should be collected during the DebuggerStop event - private bool CollectVariables = true; + private bool _collectVariables = true; // Runspace object to store the runspace that the debugger is attached to - private Runspace Runspace; + private readonly Runspace _runspace; public DebuggerHandler(Runspace runspace, bool collectVariables = true) { - // Set the collection flag and Runspace object - CollectVariables = collectVariables; - Runspace = runspace; + // Ensure the runspace is not null + if (runspace == null) + { + throw new ArgumentNullException("runspace"); // Use string literal for older C# compatibility + } + + _runspace = runspace; + _collectVariables = collectVariables; - // Initialize the event handler with the OnDebuggerStop method - DebuggerStopHandler = new EventHandler(OnDebuggerStop); - Runspace.Debugger.DebuggerStop += DebuggerStopHandler; + // Initialize the event handler + _debuggerStopHandler = OnDebuggerStop; + _runspace.Debugger.DebuggerStop += _debuggerStopHandler; + + // Initialize variables collection + Variables = new PSDataCollection(); + IsEventTriggered = false; } - // Method to detach the DebuggerStop event handler from the runspace's debugger, and general clean-up + /// + /// Detaches the DebuggerStop event handler and releases resources. + /// public void Dispose() { - IsEventTriggered = false; - - // Remove the event handler to prevent further event handling - if (DebuggerStopHandler != default(EventHandler)) + if (_debuggerStopHandler != null) { - Runspace.Debugger.DebuggerStop -= DebuggerStopHandler; - DebuggerStopHandler = null; + _runspace.Debugger.DebuggerStop -= _debuggerStopHandler; + _debuggerStopHandler = null; } - // Clean-up variables - Runspace = default(Runspace); + // Clear variables and release the runspace Variables.Clear(); - - // Garbage collection GC.SuppressFinalize(this); } - // Event handler method that gets called when the debugger stops + /// + /// Event handler for the DebuggerStop event. + /// private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) { - // Set the eventTriggered flag to true IsEventTriggered = true; // Cast the sender to a Debugger object var debugger = sender as Debugger; - if (debugger == default(Debugger)) + if (debugger == null) { return; } - // Enable step mode to allow for command execution during the debug stop + // Enable step mode for command execution debugger.SetDebuggerStepMode(true); - // Collect variables or hang the debugger + // Create the command to execute var command = new PSCommand(); - command.AddCommand(CollectVariables + command.AddCommand(_collectVariables ? "Get-PodeDumpScopedVariable" : "while($PodeContext.Server.Suspended) { Start-Sleep -Milliseconds 500 }"); @@ -79,19 +85,14 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) var outputCollection = new PSDataCollection(); debugger.ProcessCommand(command, outputCollection); - // Add results to the variables collection if collecting variables - if (CollectVariables) + // Collect the variables if required + if (_collectVariables) { foreach (var output in outputCollection) { Variables.Add(output); } } - else - { - // Ensure the debugger remains ready for further interaction - debugger.SetDebuggerStepMode(true); - } } } -} \ No newline at end of file +} From 414d785f1e80e3a83aa850415434ec2a8de6838c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 3 Dec 2024 17:02:36 -0800 Subject: [PATCH 26/34] minor changes --- src/Private/Context.ps1 | 10 ++++++---- src/Private/Server.ps1 | 21 ++++++++++++--------- src/Public/Core.ps1 | 8 +++++--- tests/unit/Server.Tests.ps1 | 9 +++++---- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 70a6d1903..d2dab04ca 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -411,10 +411,12 @@ function New-PodeContext { # create new cancellation tokens $ctx.Tokens = @{ - Cancellation = [System.Threading.CancellationTokenSource]::new() - Restart = [System.Threading.CancellationTokenSource]::new() - Dump = [System.Threading.CancellationTokenSource]::new() - SuspendResume = [System.Threading.CancellationTokenSource]::new() + Cancellation = [System.Threading.CancellationTokenSource]::new() + Restart = [System.Threading.CancellationTokenSource]::new() + Dump = [System.Threading.CancellationTokenSource]::new() + Pause = [System.Threading.CancellationTokenSource]::new() + Resume = [System.Threading.CancellationTokenSource]::new() + #Terminate = [System.Threading.CancellationTokenSource]::new() } # requests that should be logged diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index ce8f68ec2..d6cd181fe 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -377,6 +377,8 @@ function Restart-PodeInternalServer { Reset-PodeCancellationToken -Type Cancellation Reset-PodeCancellationToken -Type Restart Reset-PodeCancellationToken -Type Dump + Reset-PodeCancellationToken -Type Pause + Reset-PodeCancellationToken -Type Resume # reload the configuration $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext @@ -401,36 +403,37 @@ function Restart-PodeInternalServer { <# .SYNOPSIS Resets the cancellation token for a specific type in Pode. - .DESCRIPTION The `Reset-PodeCancellationToken` function disposes of the existing cancellation token for the specified type and reinitializes it with a new token. This ensures proper cleanup of disposable resources associated with the cancellation token. - .PARAMETER Type The type of cancellation token to reset. This is a mandatory parameter and must be provided as a string. -.EXAMPLES +.EXAMPLE # Reset the cancellation token for the 'Cancellation' type Reset-PodeCancellationToken -Type Cancellation +.EXAMPLE # Reset the cancellation token for the 'Restart' type Reset-PodeCancellationToken -Type Restart +.EXAMPLE # Reset the cancellation token for the 'Dump' type Reset-PodeCancellationToken -Type Dump - # Reset the cancellation token for the 'SuspendResume' type - Reset-PodeCancellationToken -Type SuspendResume +.EXAMPLE + # Reset the cancellation token for the 'Pause' type + Reset-PodeCancellationToken -Type Pause .NOTES This function is used to manage cancellation tokens in Pode's internal context. - #> function Reset-PodeCancellationToken { param( [Parameter(Mandatory = $true)] + [validateset( 'Cancellation' , 'Restart', 'Dump', 'Pause', 'Resume' )] [string] $Type ) @@ -515,7 +518,7 @@ function Suspend-PodeServerInternal { # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` - $_.Name -notlike '*__pode_session_inmem_cleanup__*' } | Sort-Object Name + $_.Name -notlike '*__pode_session_inmem_cleanup__*' } | Sort-Object Name foreach ($runspace in $runspaces) { try { @@ -550,7 +553,7 @@ function Suspend-PodeServerInternal { $_ | Write-PodeErrorLog } finally { - Reset-PodeCancellationToken -Type SuspendResume + Reset-PodeCancellationToken -Type Pause } } @@ -609,6 +612,6 @@ function Resume-PodeServerInternal { } finally { # Reinitialize the CancellationTokenSource for future suspension/resumption - Reset-PodeCancellationToken -Type SuspendResume + Reset-PodeCancellationToken -Type Resume } } \ No newline at end of file diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index e777c2157..852249056 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -250,12 +250,14 @@ function Start-PodeServer { Invoke-PodeDumpInternal -Format $PodeContext.Server.Debug.Dump.Format -Path $PodeContext.Server.Debug.Dump.Path -MaxDepth $PodeContext.Server.Debug.Dump.MaxDepth } - if (($PodeContext.Tokens.Suspend.SuspendResume) -or (Test-PodeSuspendPressed -Key $key)) { + if (($PodeContext.Tokens.Suspend.IsCancellationRequested) -or ($PodeContext.Tokens.Resume.IsCancellationRequested) -or (Test-PodeSuspendPressed -Key $key)) { Clear-PodeKeyPressed if ( $PodeContext.Server.Suspended) { + $PodeContext.Tokens.Resume.Cancel() Resume-PodeServerInternal } else { + $PodeContext.Tokens.Pause.Cancel() Suspend-PodeServerInternal } } @@ -401,7 +403,7 @@ function Resume-PodeServer { [CmdletBinding()] param() if ( $PodeContext.Server.Suspended) { - $PodeContext.Tokens.SuspendResume.Cancel() + $PodeContext.Tokens.Resume.Cancel() } } @@ -423,7 +425,7 @@ function Suspend-PodeServer { [CmdletBinding()] param() if (! $PodeContext.Server.Suspended) { - $PodeContext.Tokens.SuspendResume.Cancel() + $PodeContext.Tokens.Pause.Cancel() } } diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 0b756a58d..e8c5a7a7f 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -112,10 +112,11 @@ Describe 'Restart-PodeInternalServer' { It 'Resetting the server values' { $PodeContext = @{ Tokens = @{ - Cancellation = [System.Threading.CancellationTokenSource]::new() - Restart = [System.Threading.CancellationTokenSource]::new() - Dump = [System.Threading.CancellationTokenSource]::new() - SuspendResume = [System.Threading.CancellationTokenSource]::new() + Cancellation = [System.Threading.CancellationTokenSource]::new() + Restart = [System.Threading.CancellationTokenSource]::new() + Dump = [System.Threading.CancellationTokenSource]::new() + Pause = [System.Threading.CancellationTokenSource]::new() + Resume = [System.Threading.CancellationTokenSource]::new() } Server = @{ Routes = @{ From 68f14b0dda7dcccd13fd7374423fe66ddb786458 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 3 Dec 2024 17:43:12 -0800 Subject: [PATCH 27/34] tentative to use a new terminate token --- src/Private/Context.ps1 | 4 ++-- src/Private/Helpers.ps1 | 7 ++++++- src/Private/PodeServer.ps1 | 2 +- src/Private/Schedules.ps1 | 2 +- src/Private/Server.ps1 | 17 ++++++++++------- src/Public/Core.ps1 | 10 ++++++---- tests/unit/Server.Tests.ps1 | 5 +++-- 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index d2dab04ca..b19f00c11 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -414,9 +414,9 @@ function New-PodeContext { Cancellation = [System.Threading.CancellationTokenSource]::new() Restart = [System.Threading.CancellationTokenSource]::new() Dump = [System.Threading.CancellationTokenSource]::new() - Pause = [System.Threading.CancellationTokenSource]::new() + Suspend = [System.Threading.CancellationTokenSource]::new() Resume = [System.Threading.CancellationTokenSource]::new() - #Terminate = [System.Threading.CancellationTokenSource]::new() + Terminate = [System.Threading.CancellationTokenSource]::new() } # requests that should be logged diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index bba2dcb00..ae27291b1 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -747,9 +747,10 @@ function Close-PodeServerInternal { ) # ensure the token is cancelled - if ($null -ne $PodeContext.Tokens.Cancellation) { + if ($null -ne $PodeContext.Tokens.Cancellation -and $null -ne $PodeContext.Tokens.Cancellation) { Write-Verbose 'Cancelling main cancellation token' $PodeContext.Tokens.Cancellation.Cancel() + $PodeContext.Tokens.Terminate.Cancel() } # stop all current runspaces @@ -764,7 +765,11 @@ function Close-PodeServerInternal { # remove all the cancellation tokens Write-Verbose 'Disposing cancellation tokens' Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation + Close-PodeDisposable -Disposable $PodeContext.Tokens.Terminate Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart + Close-PodeDisposable -Disposable $PodeContext.Tokens.Dump + Close-PodeDisposable -Disposable $PodeContext.Tokens.Suspend + Close-PodeDisposable -Disposable $PodeContext.Tokens.Resume # dispose mutex/semaphores Write-Verbose 'Diposing mutex and semaphores' diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 34e830f0d..d0f725fca 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -293,7 +293,7 @@ function Start-PodeWebServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Web -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Web -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } } diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index decdc8bf9..007f45140 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -88,7 +88,7 @@ function Start-PodeScheduleRunspace { # complete any schedules Complete-PodeInternalSchedule -Now $_now - + # Calculate the remaining seconds to sleep until the next minute $remainingSeconds = 60 - [DateTime]::Now.Second diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index d6cd181fe..b2c8fd819 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -269,6 +269,7 @@ function Restart-PodeInternalServer { # cancel the session token $PodeContext.Tokens.Cancellation.Cancel() + $PodeContext.Tokens.Terminate.Cancel() # close all current runspaces Close-PodeRunspace -ClosePool @@ -377,8 +378,9 @@ function Restart-PodeInternalServer { Reset-PodeCancellationToken -Type Cancellation Reset-PodeCancellationToken -Type Restart Reset-PodeCancellationToken -Type Dump - Reset-PodeCancellationToken -Type Pause + Reset-PodeCancellationToken -Type Suspend Reset-PodeCancellationToken -Type Resume + Reset-PodeCancellationToken -Type Terminate # reload the configuration $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext @@ -424,8 +426,8 @@ function Restart-PodeInternalServer { Reset-PodeCancellationToken -Type Dump .EXAMPLE - # Reset the cancellation token for the 'Pause' type - Reset-PodeCancellationToken -Type Pause + # Reset the cancellation token for the 'Suspend' type + Reset-PodeCancellationToken -Type Suspend .NOTES This function is used to manage cancellation tokens in Pode's internal context. @@ -433,7 +435,7 @@ function Restart-PodeInternalServer { function Reset-PodeCancellationToken { param( [Parameter(Mandatory = $true)] - [validateset( 'Cancellation' , 'Restart', 'Dump', 'Pause', 'Resume' )] + [validateset( 'Cancellation' , 'Restart', 'Dump', 'Suspend', 'Resume', 'Terminate' )] [string] $Type ) @@ -515,6 +517,7 @@ function Suspend-PodeServerInternal { # Update the server's suspended state $PodeContext.Server.Suspended = $true + start-sleep 4 # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` @@ -553,8 +556,8 @@ function Suspend-PodeServerInternal { $_ | Write-PodeErrorLog } finally { - Reset-PodeCancellationToken -Type Pause - + Reset-PodeCancellationToken -Type Suspend + #Reset-PodeCancellationToken -Type Cancellation } } @@ -587,7 +590,7 @@ function Resume-PodeServerInternal { # Update the server's suspended state $PodeContext.Server.Suspended = $false - # Pause briefly to ensure any required internal processes have time to stabilize + # Suspend briefly to ensure any required internal processes have time to stabilize Start-Sleep -Seconds 5 # Retrieve all runspaces related to Pode diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 852249056..0d2e5e78d 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -253,11 +253,11 @@ function Start-PodeServer { if (($PodeContext.Tokens.Suspend.IsCancellationRequested) -or ($PodeContext.Tokens.Resume.IsCancellationRequested) -or (Test-PodeSuspendPressed -Key $key)) { Clear-PodeKeyPressed if ( $PodeContext.Server.Suspended) { - $PodeContext.Tokens.Resume.Cancel() + Resume-PodeServer Resume-PodeServerInternal } else { - $PodeContext.Tokens.Pause.Cancel() + Suspend-PodeServer Suspend-PodeServerInternal } } @@ -316,7 +316,7 @@ function Start-PodeServer { # Terminating... Write-PodeHost $PodeLocale.terminatingMessage -NoNewLine -ForegroundColor Yellow Invoke-PodeEvent -Type Terminate - $PodeContext.Tokens.Cancellation.Cancel() + Close-PodeServer } catch { $_ | Write-PodeErrorLog @@ -366,6 +366,7 @@ function Close-PodeServer { param() $PodeContext.Tokens.Cancellation.Cancel() + $PodeContext.Tokens.Terminate.Cancel() } <# @@ -425,7 +426,8 @@ function Suspend-PodeServer { [CmdletBinding()] param() if (! $PodeContext.Server.Suspended) { - $PodeContext.Tokens.Pause.Cancel() + $PodeContext.Tokens.Suspend.Cancel() + #$PodeContext.Tokens.Cancellation.Cancel() } } diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index e8c5a7a7f..1724fce3d 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -115,8 +115,9 @@ Describe 'Restart-PodeInternalServer' { Cancellation = [System.Threading.CancellationTokenSource]::new() Restart = [System.Threading.CancellationTokenSource]::new() Dump = [System.Threading.CancellationTokenSource]::new() - Pause = [System.Threading.CancellationTokenSource]::new() - Resume = [System.Threading.CancellationTokenSource]::new() + Suspend = [System.Threading.CancellationTokenSource]::new() + Resume = [System.Threading.CancellationTokenSource]::new() + Terminate = [System.Threading.CancellationTokenSource]::new() } Server = @{ Routes = @{ From edc604ee09b8d13387c7b0bfa0a225f749d64d60 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 5 Dec 2024 11:00:40 -0800 Subject: [PATCH 28/34] looking good --- examples/Tasks.ps1 | 2 ++ examples/Web-Dump.ps1 | 22 ++++++++++++++--- src/Private/Dump.ps1 | 46 +++++++++++++++++++++++------------ src/Private/FileWatchers.ps1 | 16 ++++++++++-- src/Private/Gui.ps1 | 2 +- src/Private/Logging.ps1 | 8 +++++- src/Private/PodeServer.ps1 | 32 +++++++++++++++++++++--- src/Private/Schedules.ps1 | 11 +++++++-- src/Private/ServiceServer.ps1 | 2 +- src/Private/SmtpServer.ps1 | 16 ++++++++++-- src/Private/TcpServer.ps1 | 16 ++++++++++-- src/Private/Timers.ps1 | 8 +++++- src/Private/WebSockets.ps1 | 21 ++++++++++++++-- src/Public/Core.ps1 | 6 +++-- src/Public/Utilities.ps1 | 43 +++++++++++++++++++++++++++++++- 15 files changed, 212 insertions(+), 39 deletions(-) diff --git a/examples/Tasks.ps1 b/examples/Tasks.ps1 index 9b98e730a..def36c56b 100644 --- a/examples/Tasks.ps1 +++ b/examples/Tasks.ps1 @@ -53,6 +53,8 @@ Start-PodeServer { Add-PodeTask -Name 'Test2' -ScriptBlock { param($value) Start-Sleep -Seconds 10 + "a $($value) is comming" | Out-Default + Start-Sleep -Seconds 100 "a $($value) is never late, it arrives exactly when it means to" | Out-Default } diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 index 6fc3cf9ef..67b7809ac 100644 --- a/examples/Web-Dump.ps1 +++ b/examples/Web-Dump.ps1 @@ -14,7 +14,7 @@ http://localhost:8081/openapi Documentation: http://localhost:8081/docs - +5 .LINK https://github.com/Badgerati/Pode/blob/develop/examples/Web-Dump.ps1 @@ -39,7 +39,7 @@ try { catch { throw } # Start Pode server with specified script block -Start-PodeServer -Threads 4 -ScriptBlock { +Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { # listen on localhost:8081 Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http @@ -94,7 +94,12 @@ Start-PodeServer -Threads 4 -ScriptBlock { } } | Set-PodeOARouteInfo -Summary 'Dump state' -Description 'Dump the memory state of the server.' -Tags 'dump' -OperationId 'dump'-PassThru | Set-PodeOARequest -Parameters (New-PodeOAStringProperty -Name 'format' -Description 'Dump export format.' -Enum 'json', 'clixml', 'txt', 'bin', 'yaml' -Default 'json' | ConvertTo-PodeOAParameter -In Query ) - } + + Add-PodeRoute -Method Get -Path '/task/async' -PassThru -ScriptBlock { + Invoke-PodeTask -Name 'Test' -ArgumentList @{ value = 'wizard' } | Out-Null + Write-PodeJsonResponse -Value @{ Result = 'jobs done' } + }| Set-PodeOARouteInfo -Summary 'Task' + } Add-PodeVerb -Verb 'HELLO' -ScriptBlock { Write-PodeTcpClient -Message 'HI' @@ -148,4 +153,15 @@ Start-PodeServer -Threads 4 -ScriptBlock { $name = Read-PodeTcpClient -CRLFMessageEnd Write-PodeTcpClient -Message "Hi, $($name)!" } + + + Add-PodeTask -Name 'Test' -ScriptBlock { + param($value) + Start-PodeSleep -Seconds 10 + "a $($value) is comming" | Out-Default + Start-PodeSleep -Seconds 100 + "a $($value) is never late, it arrives exactly when it means to" | Out-Default + } + + } \ No newline at end of file diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index c53b0dfbc..34c4e0239 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -155,7 +155,11 @@ function Invoke-PodeDumpInternal { # Retrieve all runspaces related to Pode ordered by name $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` - $_.Name -notlike '*__pode_session_inmem_cleanup__*' } | Sort-Object Name + $_.Name -notlike '*__pode_session_inmem_cleanup__*' -and ` + $_.Name -notlike 'Pode_*_Listener_*' -and ` + $_.Name -notlike 'Pode_*_KeepAlive_*' -and ` + $_.Name -notlike 'Pode_Signals_Broadcaster_*' + } | Sort-Object Name $runspaceDetails = @{} @@ -234,8 +238,8 @@ function Invoke-PodeDumpInternal { Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" } end { - - Reset-PodeCancellationToken -Type 'Dump' + Reset-PodeCancellationToken -Type Cancellation + Reset-PodeCancellationToken -Type Dump } } @@ -287,25 +291,33 @@ function Get-PodeRunspaceVariablesViaDebugger { # Initialize variables collection $variables = @(@{}) + for ($i = 0; $i -le 3; $i++) { try { - # Attach the debugger and break all + + # Wait for the event to be triggered or timeout + Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine + $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) Enable-RunspaceDebug -BreakAll -Runspace $Runspace # Wait for the event to be triggered or timeout + $startTime = [DateTime]::UtcNow + Start-Sleep -Milliseconds 500 - Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine + Write-PodeHost '.' -NoNewLine - # Suspend the runspace - if (Suspend-PodeRunspace -Runspace $Runspace) { - # Retrieve and output the collected variables from the embedded C# code - $variables = $debugger.Variables - break + while (!$debugger.IsEventTriggered) { + Start-Sleep -Milliseconds 1000 + Write-PodeHost '.' -NoNewLine + if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { + Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" + return $variables[0] + } } - + return $debugger.Variables } catch { # Log the error details using Write-PodeErrorLog. @@ -710,23 +722,26 @@ function Suspend-PodeRunspace { param ( $Runspace ) + # Attach the debugger and break all $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) Enable-RunspaceDebug -BreakAll -Runspace $Runspace + # Start-Sleep -Milliseconds 500 + # Enable-RunspaceDebug -BreakAll -Runspace $Runspace # Wait for the event to be triggered or timeout $startTime = [DateTime]::UtcNow - Start-Sleep -Milliseconds 1000 + Start-Sleep -Milliseconds 500 # Write-PodeHost '..' -NoNewLine - # Send-PodeInterrupt -Name $Runspace.Name + #Send-PodeInterrupt -Name $Runspace.Name Write-PodeHost '.' -NoNewLine while (!$debugger.IsEventTriggered) { Start-Sleep -Milliseconds 1000 - if (([int]([DateTime]::UtcNow - $startTime).TotalSeconds) % 5 -eq 0) { + <# if (([int]([DateTime]::UtcNow - $startTime).TotalSeconds) % 5 -eq 0) { if (Send-PodeInterrupt -Name $Runspace.Name) { Write-PodeHost '*' -NoNewLine } @@ -736,7 +751,8 @@ function Suspend-PodeRunspace { } else { Write-PodeHost '.' -NoNewLine - } + }#> + Write-PodeHost '.' -NoNewLine if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" return $false diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index ce0ebc4c7..2299e0d27 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -62,7 +62,13 @@ function Start-PodeFileWatcherRunspace { ) try { - while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Watcher.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } $evt = (Wait-PodeTask -Task $Watcher.GetFileEventAsync($PodeContext.Tokens.Cancellation.Token)) try { @@ -144,7 +150,13 @@ function Start-PodeFileWatcherRunspace { ) try { - while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Watcher.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } Start-Sleep -Seconds 1 } } diff --git a/src/Private/Gui.ps1 b/src/Private/Gui.ps1 index 94b772795..506e35b93 100644 --- a/src/Private/Gui.ps1 +++ b/src/Private/Gui.ps1 @@ -27,7 +27,7 @@ function Start-PodeGuiRunspace { # poll the server for a response $count = 0 - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { try { $null = Invoke-WebRequest -Method Get -Uri $uri -UseBasicParsing -ErrorAction Stop if (!$?) { diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index e2c11f81c..b79694031 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -376,7 +376,13 @@ function Start-PodeLoggingRunspace { $script = { try { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } try { # if there are no logs to process, just sleep for a few seconds - but after checking the batch if ($PodeContext.LogsToProcess.Count -eq 0) { diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index d0f725fca..67c6e655b 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -129,7 +129,13 @@ function Start-PodeWebServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } # get request and response $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) @@ -307,7 +313,13 @@ function Start-PodeWebServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } $message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token)) try { @@ -385,7 +397,13 @@ function Start-PodeWebServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } $context = (Wait-PodeTask -Task $Listener.GetClientSignalAsync($PodeContext.Tokens.Cancellation.Token)) try { @@ -464,7 +482,13 @@ function Start-PodeWebServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } Start-Sleep -Seconds 1 } } diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index 007f45140..4af978667 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -66,7 +66,13 @@ function Start-PodeScheduleRunspace { # first, sleep for a period of time to get to 00 seconds (start of minute) Start-Sleep -Seconds (60 - [DateTime]::Now.Second) - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } try { $_now = [DateTime]::Now @@ -95,7 +101,8 @@ function Start-PodeScheduleRunspace { # Loop in 5-second intervals until the remaining seconds are covered while ($remainingSeconds -gt 0) { $sleepTime = [math]::Min(5, $remainingSeconds) # Sleep for 5 seconds or remaining time - Start-Sleep -Seconds $sleepTime + # Start-Sleep -Seconds $sleepTime + Start-PodeSleep -Seconds $sleepTime $remainingSeconds -= $sleepTime } } diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index bd7fe6eca..35d934576 100644 --- a/src/Private/ServiceServer.ps1 +++ b/src/Private/ServiceServer.ps1 @@ -13,7 +13,7 @@ function Start-PodeServiceServer { $serverScript = { try { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { # the event object $script:ServiceEvent = @{ Lockable = $PodeContext.Threading.Lockables.Global diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index c3cc7cfd2..ba2e08e37 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -90,7 +90,13 @@ function Start-PodeSmtpServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } # get email $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) @@ -189,7 +195,13 @@ function Start-PodeSmtpServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } Start-Sleep -Seconds 1 } } diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 43a3c38c5..dc41f67ae 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -86,7 +86,13 @@ function Start-PodeTcpServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } # get email $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) @@ -207,7 +213,13 @@ function Start-PodeTcpServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } Start-Sleep -Seconds 1 } } diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index 48b19ba9a..f6bbd032c 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -21,7 +21,13 @@ function Start-PodeTimerRunspace { $script = { try { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } try { $_now = [DateTime]::Now diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index b1c0cdc8f..1d82ee09d 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -52,7 +52,15 @@ function Start-PodeWebSocketRunspace { ) try { - while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Receiver.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + + while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + write-podehost 'checking for PodeContext.Tokens.Dump.IsCancellationRequested' + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } # get request $request = (Wait-PodeTask -Task $Receiver.GetWebSocketRequestAsync($PodeContext.Tokens.Cancellation.Token)) @@ -119,9 +127,18 @@ function Start-PodeWebSocketRunspace { ) try { - while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Receiver.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + write-podehost 'checking for PodeContext.Tokens.Dump.IsCancellationRequested' + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } Start-Sleep -Seconds 1 } + + } catch [System.OperationCanceledException] { $_ | Write-PodeErrorLog -Level Debug diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 0d2e5e78d..79563a305 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -231,7 +231,7 @@ function Start-PodeServer { } # sit here waiting for termination/cancellation, or to restart the server - while ( !($PodeContext.Tokens.Cancellation.IsCancellationRequested)) { + while ( !($PodeContext.Tokens.Terminate.IsCancellationRequested)) { Start-Sleep -Seconds 1 if (!$PodeContext.Server.Console.DisableConsoleInput) { @@ -247,6 +247,7 @@ function Start-PodeServer { if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { Clear-PodeKeyPressed + Invoke-PodeDump Invoke-PodeDumpInternal -Format $PodeContext.Server.Debug.Dump.Format -Path $PodeContext.Server.Debug.Dump.Path -MaxDepth $PodeContext.Server.Debug.Dump.MaxDepth } @@ -405,6 +406,7 @@ function Resume-PodeServer { param() if ( $PodeContext.Server.Suspended) { $PodeContext.Tokens.Resume.Cancel() + $PodeContext.Tokens.Cancellation.Cancel() } } @@ -426,8 +428,8 @@ function Suspend-PodeServer { [CmdletBinding()] param() if (! $PodeContext.Server.Suspended) { + $PodeContext.Tokens.Cancellation.Cancel() $PodeContext.Tokens.Suspend.Cancel() - #$PodeContext.Tokens.Cancellation.Cancel() } } diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 9e6171bcd..f89340c58 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1553,4 +1553,45 @@ function Invoke-PodeDump { ) $PodeContext.Server.Debug.Dump.Param = $PSBoundParameters $PodeContext.Tokens.Dump.Cancel() -} \ No newline at end of file + $PodeContext.Tokens.Cancellation.Cancel() +} + + + +function Start-PodeSleep { + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $false, ParameterSetName = 'Seconds')] + [int]$Seconds = 1, + + [Parameter(Position = 0, Mandatory = $false, ParameterSetName = 'Milliseconds')] + [int]$Milliseconds, + + [Parameter(Position = 0, Mandatory = $false, ParameterSetName = 'Duration')] + [TimeSpan]$Duration + ) + + # Determine end time based on the parameter set + switch ($PSCmdlet.ParameterSetName) { + 'Seconds' { + $endTime = (Get-Date).AddSeconds($Seconds) + } + 'Milliseconds' { + $endTime = (Get-Date).AddMilliseconds($Milliseconds) + } + 'Duration' { + $endTime = (Get-Date).Add($Duration) + } + } + + while ((Get-Date) -lt $endTime) { + # Check if a debugger is attached + # if ($Host.Debugger.IsActive) { + # Write-PodeHost "Debugger is attached. Waiting for interaction..." + # Debugger # Trigger a breakpoint to allow interaction + # } + + # Sleep for a short duration to prevent high CPU usage + Start-Sleep -Milliseconds 200 + } +} From 0081477e51d54e822135ac80db5479cc7017524b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 6 Dec 2024 10:01:04 -0800 Subject: [PATCH 29/34] Fix the suspend and dump --- examples/Web-Dump.ps1 | 26 +- src/Private/Dump.ps1 | 370 ++++++------------------- src/Private/FileWatchers.ps1 | 132 +++++---- src/Private/Logging.ps1 | 10 +- src/Private/PodeServer.ps1 | 509 +++++++++++++++++------------------ src/Private/Schedules.ps1 | 10 +- src/Private/Server.ps1 | 57 ++-- src/Private/SmtpServer.ps1 | 159 ++++++----- src/Private/TcpServer.ps1 | 189 +++++++------ src/Private/Timers.ps1 | 12 +- src/Private/WebSockets.ps1 | 108 ++++---- src/Public/Core.ps1 | 70 ++--- 12 files changed, 692 insertions(+), 960 deletions(-) diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 index 67b7809ac..b4067de13 100644 --- a/examples/Web-Dump.ps1 +++ b/examples/Web-Dump.ps1 @@ -45,7 +45,7 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Https -SelfSigned Add-PodeEndpoint -Address localhost -Port 8083 -Protocol Http - #Add-PodeEndpoint -Address localhost -Port 8025 -Protocol Smtp + Add-PodeEndpoint -Address localhost -Port 8025 -Protocol Smtp Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -Name 'WS1' Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Http -Name 'WS' Add-PodeEndpoint -Address localhost -Port 8100 -Protocol Tcp @@ -95,11 +95,11 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { } | Set-PodeOARouteInfo -Summary 'Dump state' -Description 'Dump the memory state of the server.' -Tags 'dump' -OperationId 'dump'-PassThru | Set-PodeOARequest -Parameters (New-PodeOAStringProperty -Name 'format' -Description 'Dump export format.' -Enum 'json', 'clixml', 'txt', 'bin', 'yaml' -Default 'json' | ConvertTo-PodeOAParameter -In Query ) - Add-PodeRoute -Method Get -Path '/task/async' -PassThru -ScriptBlock { - Invoke-PodeTask -Name 'Test' -ArgumentList @{ value = 'wizard' } | Out-Null - Write-PodeJsonResponse -Value @{ Result = 'jobs done' } - }| Set-PodeOARouteInfo -Summary 'Task' - } + Add-PodeRoute -Method Get -Path '/task/async' -PassThru -ScriptBlock { + Invoke-PodeTask -Name 'Test' -ArgumentList @{ value = 'wizard' } | Out-Null + Write-PodeJsonResponse -Value @{ Result = 'jobs done' } + } | Set-PodeOARouteInfo -Summary 'Task' + } Add-PodeVerb -Verb 'HELLO' -ScriptBlock { Write-PodeTcpClient -Message 'HI' @@ -107,7 +107,7 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { } # setup an smtp handler - <# Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { + Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { Write-PodeHost '- - - - - - - - - - - - - - - - - -' Write-PodeHost $SmtpEvent.Email.From Write-PodeHost $SmtpEvent.Email.To @@ -126,7 +126,7 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { $SmtpEvent.Email.Headers | out-default Write-PodeHost '- - - - - - - - - - - - - - - - - -' } -#> + # GET request for web page Add-PodeRoute -Method Get -Path '/' -EndpointName 'WS' -ScriptBlock { Write-PodeViewResponse -Path 'websockets' @@ -159,6 +159,16 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { param($value) Start-PodeSleep -Seconds 10 "a $($value) is comming" | Out-Default + Start-PodeSleep -Seconds 10 + "a $($value) is comming...2" | Out-Default + Start-PodeSleep -Seconds 10 + "a $($value) is comming...3" | Out-Default + Start-PodeSleep -Seconds 10 + "a $($value) is comming...4" | Out-Default + Start-PodeSleep -Seconds 10 + "a $($value) is comming...5" | Out-Default + Start-PodeSleep -Seconds 10 + "a $($value) is comming...6" | Out-Default Start-PodeSleep -Seconds 100 "a $($value) is never late, it arrives exactly when it means to" | Out-Default } diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 34c4e0239..a9719750d 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -156,9 +156,9 @@ function Invoke-PodeDumpInternal { # Retrieve all runspaces related to Pode ordered by name $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` $_.Name -notlike '*__pode_session_inmem_cleanup__*' -and ` - $_.Name -notlike 'Pode_*_Listener_*' -and ` - $_.Name -notlike 'Pode_*_KeepAlive_*' -and ` - $_.Name -notlike 'Pode_Signals_Broadcaster_*' + # $_.Name -notlike 'Pode_*_Listener_*' -and ` + $_.Name -notlike 'Pode_*_KeepAlive_*' #-and ` + # $_.Name -notlike 'Pode_Signals_Broadcaster_*' } | Sort-Object Name @@ -168,7 +168,7 @@ function Invoke-PodeDumpInternal { Id = $r.Id Name = @{ $r.Name = @{ - ScopedVariables = Get-PodeRunspaceVariablesViaDebugger -Runspace $r -NumberOfRunspaces $runspaces.Count + ScopedVariables = Suspend-PodeRunspace -Runspace $r -NumberOfRunspaces $runspaces.Count } } InitialSessionState = $r.InitialSessionState @@ -238,8 +238,8 @@ function Invoke-PodeDumpInternal { Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" } end { - Reset-PodeCancellationToken -Type Cancellation - Reset-PodeCancellationToken -Type Dump + + Reset-PodeCancellationToken -Type Cancellation, Dump } } @@ -273,7 +273,7 @@ function Invoke-PodeDumpInternal { The function uses an embedded C# class to handle the `DebuggerStop` event. This class attaches and detaches the debugger and processes commands in the stopped state. The collected variables are returned as a `PSObject`. #> -function Get-PodeRunspaceVariablesViaDebugger { +function Suspend-PodeRunspace { param ( [Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.Runspace] @@ -285,57 +285,65 @@ function Get-PodeRunspaceVariablesViaDebugger { [Parameter(Mandatory = $true)] [int] - $NumberOfRunspaces - - ) - - # Initialize variables collection - $variables = @(@{}) + $NumberOfRunspaces, - for ($i = 0; $i -le 3; $i++) { + [switch] + $CollectVariable - try { + ) + try { + # Wait for the event to be triggered or timeout + Write-PodeHost "Waiting for $($Runspace.Name) to be suspended." -NoNewLine - # Wait for the event to be triggered or timeout - Write-PodeHost "Waiting for $($Runspace.Name) to enter in debug ." -NoNewLine + $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) + Enable-RunspaceDebug -BreakAll -Runspace $Runspace - $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) - Enable-RunspaceDebug -BreakAll -Runspace $Runspace + # Wait for the event to be triggered or timeout + $startTime = [DateTime]::UtcNow + Start-Sleep -Milliseconds 500 - # Wait for the event to be triggered or timeout - $startTime = [DateTime]::UtcNow - Start-Sleep -Milliseconds 500 + Write-PodeHost '.' -NoNewLine + while (!$debugger.IsEventTriggered) { + Start-Sleep -Milliseconds 1000 Write-PodeHost '.' -NoNewLine - - while (!$debugger.IsEventTriggered) { - Start-Sleep -Milliseconds 1000 - Write-PodeHost '.' -NoNewLine - if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { - Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" - return $variables[0] - } + if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { + Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" + if ( $CollectVariable) { return @{} }else { return $false } } + } + Write-PodeHost 'Done' + if ( $CollectVariable) { + # Return the collected variables return $debugger.Variables } - catch { - # Log the error details using Write-PodeErrorLog. - # This ensures that any exceptions thrown during the execution are logged appropriately. - $_ | Write-PodeErrorLog + else { + return $true } - finally { - # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. - if ($null -ne $debugger) { - $debugger.Dispose() - } - + } + catch { + # Log the error details using Write-PodeErrorLog. + # This ensures that any exceptions thrown during the execution are logged appropriately. + $_ | Write-PodeErrorLog + } + finally { + # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. + if ($null -ne $debugger) { + $debugger.Dispose() + } + if ($CollectVariable) { # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. Disable-RunspaceDebug -Runspace $Runspace } } - return $variables[0] + if ( $CollectVariable) { + return @{} + } + else { + return $false + } } @@ -496,269 +504,47 @@ function Initialize-PodeDebugHandler { } } -function Send-PodeTelnetCommand { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string]$ServerUri, # Server URI in the format tcp://hostname:port or tcps://hostname:port - [Parameter(Mandatory = $true)] - [string]$Command, # The string command to send (e.g., "HELO domain.com") - [int]$Timeout = 2000 # Timeout in milliseconds - ) - try { - # Parse the ServerUri to extract the host and port - $uri = [System.Uri]::new($ServerUri) - if ($uri.Scheme -notin @('tcp', 'tcps')) { - Write-Verbose "Invalid URI scheme. Expected 'tcp://' or 'tcps://', but got '$($uri.Scheme)://'." - return - } - $server = $uri.Host - $port = $uri.Port - - # Create a TCP client and connect to the server - $tcpClient = [System.Net.Sockets.TcpClient]::new() - $tcpClient.Connect($server, $port) - $tcpClient.ReceiveTimeout = $Timeout - $tcpClient.SendTimeout = $Timeout - - # Get the network stream - $stream = $tcpClient.GetStream() - - # Wrap the stream in SslStream for TCPS connections - if ($uri.Scheme -eq 'tcps') { - $sslStream = [System.Net.Security.SslStream]::new($stream, $false, { $true }) # Simple certificate validation - $sslStream.AuthenticateAsClient($server) - $stream = $sslStream - } - # Create a stream writer and reader for the connection - $writer = [System.IO.StreamWriter]::new($stream) - $reader = [System.IO.StreamReader]::new($stream) - - # Send the command - Write-Verbose "Sending command: $Command" - $writer.WriteLine($Command) - $writer.Flush() - - # Read the response - $response = @() - while ($reader.Peek() -ge 0) { - $response += $reader.ReadLine() - } - - # Close the connection - $writer.Close() - $reader.Close() - $stream.Close() - $tcpClient.Close() - - # Return the response - return $response -join "`n" - } - catch { - Write-Verbose "An error occurred: $_" - } -} - - - -function Send-PodeWebSocketMessage { - param ( - [Parameter(Mandatory = $true)] - [string] - $Message, - - [Parameter(Mandatory = $true)] - [string] - $ServerUri - ) - - # Load the WebSocket Client - # Add-Type -AssemblyName System.Net.Http - - # Initialize the WebSocket client - $clientWebSocket = [System.Net.WebSockets.ClientWebSocket]::new() - - try { - # Connect to the WebSocket server - Write-Verbose "Connecting to WebSocket server at $ServerUri..." - $clientWebSocket.ConnectAsync([uri]::new($ServerUri), [System.Threading.CancellationToken]::None).Wait() - Write-Verbose 'Connected to WebSocket server.' - - # Convert the message to bytes - $buffer = [System.Text.Encoding]::UTF8.GetBytes($Message) - $segment = [System.ArraySegment[byte]]::new($buffer) - - # Send the message - Write-Verbose "Sending message: $Message" - $clientWebSocket.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, [System.Threading.CancellationToken]::None).Wait() - Write-Verbose 'Message sent successfully.' - - # Optional: Receive a response (if expected) - $responseBuffer = [byte[]]::new( 1024) - $responseSegment = [System.ArraySegment[byte]]::new($responseBuffer) - $result = $clientWebSocket.ReceiveAsync($responseSegment, [System.Threading.CancellationToken]::None).Result - - # Decode and display the response message - $responseMessage = [System.Text.Encoding]::UTF8.GetString($responseBuffer, 0, $result.Count) - Write-Verbose "Received response: $responseMessage" - - } - catch { - Write-Error "An error occurred: $_" - } - finally { - # Close the WebSocket connection - try { - $clientWebSocket.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Closing', [System.Threading.CancellationToken]::None).Wait() - Write-Verbose 'WebSocket connection closed.' - } - catch { - Write-Verbose "Failed to close WebSocket connection: $_" - } - - # Dispose of the WebSocket client - $clientWebSocket.Dispose() - } -} - -function Send-PodeInterrupt { - param ( - [string] - $Name - ) - - if ($Name.StartsWith('Pode_Web_Listener') ) { - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and !$_.Url.StartsWith('https') }).Url - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - try { - Invoke-WebRequest -Uri $uri -ErrorAction SilentlyContinue > $null 2>&1 - } - catch { - # Suppress any exceptions - Write-Verbose -Message $_ - } - } - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Web' -and $_.Url.StartsWith('https') }).Url - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - try { - Invoke-WebRequest -Uri $uri -SkipCertificateCheck -ErrorAction SilentlyContinue > $null 2>&1 - } - catch { - # Suppress any exceptions - Write-Verbose -Message $_ - } - } - } - elseif ($Name.StartsWith('Pode_Smtp_Listener')) { - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Smtp' }).Url +<# +.SYNOPSIS + Waits for Pode suspension or dump cancellation tokens to be reset. - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - Send-PodeTelnetCommand -ServerUri $uri -command "HELO domain.com`n" - } - } - elseif ($Name.StartsWith('Pode_Tcp_Listener')) { - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Tcp' }).Url - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - Send-PodeTelnetCommand -ServerUri $uri -command "`n" - } +.DESCRIPTION + The `Test-PodeSuspensionToken` function checks the status of cancellation tokens in the `$PodeContext`. + It enters a loop to wait for the `Suspend` or `Dump` cancellation tokens to be reset before proceeding. + Each loop iteration includes a 1-second delay to minimize resource usage. + Returns a boolean indicating whether the suspension or dump was initially requested. - } - elseif ($Name.StartsWith('Pode_Signals_Broadcaster')) { - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Signals' }).Url - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } +.PARAMETER None + This function does not accept any parameters. - Send-PodeWebSocketMessage -ServerUri $uri -Message '{"message":"Broadcast from PowerShell!"}' - } +.EXAMPLE + Test-PodeSuspensionToken - } - elseif ( $Name.StartsWith('Pode_Signals_Listener')) { - $uri = $PodeContext.Server.EndpointsInfo.Where({ $_.Pool -eq 'Signals' }).Url - if ($uri) { - if ( $uri -is [array]) { - $uri = $uri[0] - } - # $newuri = 'http' + $uri.Substring(2) #deal with both http and https - try { - # Invoke-WebRequest -Uri $newuri -ErrorAction SilentlyContinue -SkipCertificateCheck > $null 2>&1 + Waits for the suspension or dump tokens to be reset in the Pode context. +.OUTPUTS + [bool] - Indicates whether either the suspension or dump was initially requested. - Send-PodeWebSocketMessage -ServerUri $uri -Message '{"message":"Broadcast from PowerShell!"}' - } - catch { - # Suppress any exceptions - Write-Verbose -Message $_ - } - } +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeSuspensionToken { + # Check if either Suspend or Dump tokens are initially requested + $suspended = $PodeContext.Tokens.Suspend.IsCancellationRequested -or $PodeContext.Tokens.Dump.IsCancellationRequested + # Wait for the Suspend token to be reset + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 } - else { - return $false - } - - Return $true -} - - -function Suspend-PodeRunspace { - param ( - $Runspace - ) - - # Attach the debugger and break all - $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) - Enable-RunspaceDebug -BreakAll -Runspace $Runspace - # Start-Sleep -Milliseconds 500 - # Enable-RunspaceDebug -BreakAll -Runspace $Runspace - - # Wait for the event to be triggered or timeout - $startTime = [DateTime]::UtcNow - Start-Sleep -Milliseconds 500 - # Write-PodeHost '..' -NoNewLine - #Send-PodeInterrupt -Name $Runspace.Name - - Write-PodeHost '.' -NoNewLine - - while (!$debugger.IsEventTriggered) { - Start-Sleep -Milliseconds 1000 - - <# if (([int]([DateTime]::UtcNow - $startTime).TotalSeconds) % 5 -eq 0) { - if (Send-PodeInterrupt -Name $Runspace.Name) { - Write-PodeHost '*' -NoNewLine - } - else { - Write-PodeHost '.' -NoNewLine - } - } - else { - Write-PodeHost '.' -NoNewLine - }#> - Write-PodeHost '.' -NoNewLine - if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { - Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" - return $false - } + # Wait for the Dump token to be reset + while ($PodeContext.Tokens.Dump.IsCancellationRequested) { + Start-Sleep -Seconds 1 } - - Write-PodeHost 'Done' - return $true + # Return whether suspension or dump was initially requested + return $suspended } \ No newline at end of file diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index 2299e0d27..2b0fb9849 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -60,82 +60,80 @@ function Start-PodeFileWatcherRunspace { [int] $ThreadId ) + do { + try { + while ($Watcher.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + $evt = (Wait-PodeTask -Task $Watcher.GetFileEventAsync($PodeContext.Tokens.Cancellation.Token)) - try { - while ($Watcher.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - $evt = (Wait-PodeTask -Task $Watcher.GetFileEventAsync($PodeContext.Tokens.Cancellation.Token)) - - try { try { - # get file watcher - $fileWatcher = $PodeContext.Fim.Items[$evt.FileWatcher.Name] - if ($null -eq $fileWatcher) { - continue - } + try { + # get file watcher + $fileWatcher = $PodeContext.Fim.Items[$evt.FileWatcher.Name] + if ($null -eq $fileWatcher) { + continue + } - # if there are exclusions, and one matches, return - $exc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Exclude) - if (($null -ne $exc) -and ($evt.Name -imatch $exc)) { - continue - } + # if there are exclusions, and one matches, return + $exc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Exclude) + if (($null -ne $exc) -and ($evt.Name -imatch $exc)) { + continue + } - # if there are inclusions, and none match, return - $inc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Include) - if (($null -ne $inc) -and ($evt.Name -inotmatch $inc)) { - continue - } + # if there are inclusions, and none match, return + $inc = (Convert-PodePathPatternsToRegex -Paths $fileWatcher.Include) + if (($null -ne $inc) -and ($evt.Name -inotmatch $inc)) { + continue + } - # set file event object - $FileEvent = @{ - Type = $evt.ChangeType - FullPath = $evt.FullPath - Name = $evt.Name - Old = @{ - FullPath = $evt.OldFullPath - Name = $evt.OldName + # set file event object + $FileEvent = @{ + Type = $evt.ChangeType + FullPath = $evt.FullPath + Name = $evt.Name + Old = @{ + FullPath = $evt.OldFullPath + Name = $evt.OldName + } + Parameters = @{} + Lockable = $PodeContext.Threading.Lockables.Global + Timestamp = [datetime]::UtcNow + Metadata = @{} } - Parameters = @{} - Lockable = $PodeContext.Threading.Lockables.Global - Timestamp = [datetime]::UtcNow - Metadata = @{} - } - # do we have any parameters? - if ($fileWatcher.Placeholders.Exist -and ($FileEvent.FullPath -imatch $fileWatcher.Placeholders.Path)) { - $FileEvent.Parameters = $Matches - } + # do we have any parameters? + if ($fileWatcher.Placeholders.Exist -and ($FileEvent.FullPath -imatch $fileWatcher.Placeholders.Path)) { + $FileEvent.Parameters = $Matches + } - # invoke main script - $null = Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug + # invoke main script + $null = Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat + } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + } } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException + finally { + $FileEvent = $null + Close-PodeDisposable -Disposable $evt } } - finally { - $FileEvent = $null - Close-PodeDisposable -Disposable $evt - } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw $_.Exception - } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw $_.Exception + } + + # end do-while + } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active + } 1..$PodeContext.Threads.Files | ForEach-Object { @@ -151,12 +149,6 @@ function Start-PodeFileWatcherRunspace { try { while ($Watcher.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } Start-Sleep -Seconds 1 } } diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index b79694031..5a721a787 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -377,12 +377,10 @@ function Start-PodeLoggingRunspace { $script = { try { while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } + + # Check for suspension or dump tokens and wait for the debugger to reset if active + Test-PodeSuspensionToken + try { # if there are no logs to process, just sleep for a few seconds - but after checking the batch if ($PodeContext.LogsToProcess.Count -eq 0) { diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 67c6e655b..43be04485 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -127,174 +127,172 @@ function Start-PodeWebServer { [int] $ThreadId ) + do { + try { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + # get request and response + $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - # get request and response - $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) - - try { try { - $Request = $context.Request - $Response = $context.Response - - # reset with basic event data - $WebEvent = @{ - OnEnd = @() - Auth = @{} - Response = $Response - Request = $Request - Lockable = $PodeContext.Threading.Lockables.Global - Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath) - Method = $Request.HttpMethod.ToLowerInvariant() - Query = $null - Endpoint = @{ - Protocol = $Request.Url.Scheme - Address = $Request.Host - Name = $context.EndpointName + try { + $Request = $context.Request + $Response = $context.Response + + # reset with basic event data + $WebEvent = @{ + OnEnd = @() + Auth = @{} + Response = $Response + Request = $Request + Lockable = $PodeContext.Threading.Lockables.Global + Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath) + Method = $Request.HttpMethod.ToLowerInvariant() + Query = $null + Endpoint = @{ + Protocol = $Request.Url.Scheme + Address = $Request.Host + Name = $context.EndpointName + } + ContentType = $Request.ContentType + ErrorType = $null + Cookies = @{} + PendingCookies = @{} + Parameters = $null + Data = $null + Files = $null + Streamed = $true + Route = $null + StaticContent = $null + Timestamp = [datetime]::UtcNow + TransferEncoding = $null + AcceptEncoding = $null + Ranges = $null + Sse = $null + Metadata = @{} } - ContentType = $Request.ContentType - ErrorType = $null - Cookies = @{} - PendingCookies = @{} - Parameters = $null - Data = $null - Files = $null - Streamed = $true - Route = $null - StaticContent = $null - Timestamp = [datetime]::UtcNow - TransferEncoding = $null - AcceptEncoding = $null - Ranges = $null - Sse = $null - Metadata = @{} - } - # if iis, and we have an app path, alter it - if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Path.IsNonRoot) { - $WebEvent.Path = ($WebEvent.Path -ireplace $PodeContext.Server.IIS.Path.Pattern, '') - if ([string]::IsNullOrEmpty($WebEvent.Path)) { - $WebEvent.Path = '/' + # if iis, and we have an app path, alter it + if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Path.IsNonRoot) { + $WebEvent.Path = ($WebEvent.Path -ireplace $PodeContext.Server.IIS.Path.Pattern, '') + if ([string]::IsNullOrEmpty($WebEvent.Path)) { + $WebEvent.Path = '/' + } } - } - # accept/transfer encoding - $WebEvent.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding (Get-PodeHeader -Name 'Transfer-Encoding') -ThrowError) - $WebEvent.AcceptEncoding = (Get-PodeAcceptEncoding -AcceptEncoding (Get-PodeHeader -Name 'Accept-Encoding') -ThrowError) - $WebEvent.Ranges = (Get-PodeRange -Range (Get-PodeHeader -Name 'Range') -ThrowError) + # accept/transfer encoding + $WebEvent.TransferEncoding = (Get-PodeTransferEncoding -TransferEncoding (Get-PodeHeader -Name 'Transfer-Encoding') -ThrowError) + $WebEvent.AcceptEncoding = (Get-PodeAcceptEncoding -AcceptEncoding (Get-PodeHeader -Name 'Accept-Encoding') -ThrowError) + $WebEvent.Ranges = (Get-PodeRange -Range (Get-PodeHeader -Name 'Range') -ThrowError) - # add logging endware for post-request - Add-PodeRequestLogEndware -WebEvent $WebEvent - - # stop now if the request has an error - if ($Request.IsAborted) { - throw $Request.Error - } + # add logging endware for post-request + Add-PodeRequestLogEndware -WebEvent $WebEvent - # if we have an sse clientId, verify it and then set details in WebEvent - if ($WebEvent.Request.HasSseClientId) { - if (!(Test-PodeSseClientIdValid)) { - throw [Pode.PodeRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)") + # stop now if the request has an error + if ($Request.IsAborted) { + throw $Request.Error } - if (![string]::IsNullOrEmpty($WebEvent.Request.SseClientName) -and !(Test-PodeSseClientId -Name $WebEvent.Request.SseClientName -ClientId $WebEvent.Request.SseClientId)) { - throw [Pode.PodeRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)", 404) - } + # if we have an sse clientId, verify it and then set details in WebEvent + if ($WebEvent.Request.HasSseClientId) { + if (!(Test-PodeSseClientIdValid)) { + throw [Pode.PodeRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)") + } - $WebEvent.Sse = @{ - Name = $WebEvent.Request.SseClientName - Group = $WebEvent.Request.SseClientGroup - ClientId = $WebEvent.Request.SseClientId - LastEventId = $null - IsLocal = $false - } - } + if (![string]::IsNullOrEmpty($WebEvent.Request.SseClientName) -and !(Test-PodeSseClientId -Name $WebEvent.Request.SseClientName -ClientId $WebEvent.Request.SseClientId)) { + throw [Pode.PodeRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)", 404) + } - # invoke global and route middleware - if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) { - # has the request been aborted - if ($Request.IsAborted) { - throw $Request.Error + $WebEvent.Sse = @{ + Name = $WebEvent.Request.SseClientName + Group = $WebEvent.Request.SseClientGroup + ClientId = $WebEvent.Request.SseClientId + LastEventId = $null + IsLocal = $false + } } - if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) { + # invoke global and route middleware + if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) { # has the request been aborted if ($Request.IsAborted) { throw $Request.Error } - # invoke the route - if ($null -ne $WebEvent.StaticContent) { - $fileBrowser = $WebEvent.Route.FileBrowser - if ($WebEvent.StaticContent.IsDownload) { - Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser + if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) { + # has the request been aborted + if ($Request.IsAborted) { + throw $Request.Error } - elseif ($WebEvent.StaticContent.RedirectToDefault) { - $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) - Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)" + + # invoke the route + if ($null -ne $WebEvent.StaticContent) { + $fileBrowser = $WebEvent.Route.FileBrowser + if ($WebEvent.StaticContent.IsDownload) { + Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser + } + elseif ($WebEvent.StaticContent.RedirectToDefault) { + $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) + Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)" + } + else { + $cachable = $WebEvent.StaticContent.IsCachable + Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge ` + -Cache:$cachable -FileBrowser:$fileBrowser + } } - else { - $cachable = $WebEvent.StaticContent.IsCachable - Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge ` - -Cache:$cachable -FileBrowser:$fileBrowser + elseif ($null -ne $WebEvent.Route.Logic) { + $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments ` + -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } - elseif ($null -ne $WebEvent.Route.Logic) { - $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments ` - -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat - } } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch [Pode.PodeRequestException] { - if ($Response.StatusCode -ge 500) { - $_.Exception | Write-PodeErrorLog -CheckInnerException + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug } + catch [Pode.PodeRequestException] { + if ($Response.StatusCode -ge 500) { + $_.Exception | Write-PodeErrorLog -CheckInnerException + } + + $code = $_.Exception.StatusCode + if ($code -le 0) { + $code = 400 + } - $code = $_.Exception.StatusCode - if ($code -le 0) { - $code = 400 + Set-PodeResponseStatus -Code $code -Exception $_ + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + Set-PodeResponseStatus -Code 500 -Exception $_ + } + finally { + Update-PodeServerRequestMetric -WebEvent $WebEvent } - Set-PodeResponseStatus -Code $code -Exception $_ - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - Set-PodeResponseStatus -Code 500 -Exception $_ + # invoke endware specifc to the current web event + $_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware)) + Invoke-PodeEndware -Endware $_endware } finally { - Update-PodeServerRequestMetric -WebEvent $WebEvent + $WebEvent = $null + Close-PodeDisposable -Disposable $context } - - # invoke endware specifc to the current web event - $_endware = ($WebEvent.OnEnd + @($PodeContext.Server.Endware)) - Invoke-PodeEndware -Endware $_endware - } - finally { - $WebEvent = $null - Close-PodeDisposable -Disposable $context } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw $_.Exception - } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw $_.Exception + } + + # end do-while + } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active + } # start the runspace for listening on x-number of threads @@ -311,73 +309,71 @@ function Start-PodeWebServer { [Parameter(Mandatory = $true)] $Listener ) + do { + try { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + $message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token)) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - $message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token)) - - try { - # get the sockets for the message - $sockets = @() + try { + # get the sockets for the message + $sockets = @() - # by clientId - if (![string]::IsNullOrWhiteSpace($message.ClientId)) { - $sockets = @($Listener.Signals[$message.ClientId]) - } - else { - $sockets = @($Listener.Signals.Values) - - # by path - if (![string]::IsNullOrWhiteSpace($message.Path)) { - $sockets = @(foreach ($socket in $sockets) { - if ($socket.Path -ieq $message.Path) { - $socket - } - }) + # by clientId + if (![string]::IsNullOrWhiteSpace($message.ClientId)) { + $sockets = @($Listener.Signals[$message.ClientId]) + } + else { + $sockets = @($Listener.Signals.Values) + + # by path + if (![string]::IsNullOrWhiteSpace($message.Path)) { + $sockets = @(foreach ($socket in $sockets) { + if ($socket.Path -ieq $message.Path) { + $socket + } + }) + } } - } - - # do nothing if no socket found - if (($null -eq $sockets) -or ($sockets.Length -eq 0)) { - continue - } - # send the message to all found sockets - foreach ($socket in $sockets) { - try { - $null = Wait-PodeTask -Task $socket.Context.Response.SendSignal($message) + # do nothing if no socket found + if (($null -eq $sockets) -or ($sockets.Length -eq 0)) { + continue } - catch { - $null = $Listener.Signals.Remove($socket.ClientId) + + # send the message to all found sockets + foreach ($socket in $sockets) { + try { + $null = Wait-PodeTask -Task $socket.Context.Response.SendSignal($message) + } + catch { + $null = $Listener.Signals.Remove($socket.ClientId) + } } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - } - finally { - Close-PodeDisposable -Disposable $message + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + } + finally { + Close-PodeDisposable -Disposable $message + } } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw $_.Exception - } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw $_.Exception + } + + # end do-while + } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active + } Add-PodeRunspace -Type Signals -Name 'Listener' -ScriptBlock $signalScript -Parameters @{ 'Listener' = $listener } @@ -396,75 +392,74 @@ function Start-PodeWebServer { $ThreadId ) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - $context = (Wait-PodeTask -Task $Listener.GetClientSignalAsync($PodeContext.Tokens.Cancellation.Token)) - - try { - $payload = ($context.Message | ConvertFrom-Json) - $Request = $context.Signal.Context.Request - $Response = $context.Signal.Context.Response - - $SignalEvent = @{ - Response = $Response - Request = $Request - Lockable = $PodeContext.Threading.Lockables.Global - Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath) - Data = @{ - Path = [System.Web.HttpUtility]::UrlDecode($payload.path) - Message = $payload.message - ClientId = $payload.clientId - Direct = [bool]$payload.direct - } - Endpoint = @{ - Protocol = $Request.Url.Scheme - Address = $Request.Host - Name = $context.Signal.Context.EndpointName + do { + try { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + $context = (Wait-PodeTask -Task $Listener.GetClientSignalAsync($PodeContext.Tokens.Cancellation.Token)) + + try { + $payload = ($context.Message | ConvertFrom-Json) + $Request = $context.Signal.Context.Request + $Response = $context.Signal.Context.Response + + $SignalEvent = @{ + Response = $Response + Request = $Request + Lockable = $PodeContext.Threading.Lockables.Global + Path = [System.Web.HttpUtility]::UrlDecode($Request.Url.AbsolutePath) + Data = @{ + Path = [System.Web.HttpUtility]::UrlDecode($payload.path) + Message = $payload.message + ClientId = $payload.clientId + Direct = [bool]$payload.direct + } + Endpoint = @{ + Protocol = $Request.Url.Scheme + Address = $Request.Host + Name = $context.Signal.Context.EndpointName + } + Route = $null + ClientId = $context.Signal.ClientId + Timestamp = $context.Timestamp + Streamed = $true + Metadata = @{} } - Route = $null - ClientId = $context.Signal.ClientId - Timestamp = $context.Timestamp - Streamed = $true - Metadata = @{} - } - # see if we have a route and invoke it, otherwise auto-send - $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name + # see if we have a route and invoke it, otherwise auto-send + $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name - if ($null -ne $SignalEvent.Route) { - $null = Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables -Scoped -Splat + if ($null -ne $SignalEvent.Route) { + $null = Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables -Scoped -Splat + } + else { + Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId + } } - else { - Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + } + finally { + Update-PodeServerSignalMetric -SignalEvent $SignalEvent + Close-PodeDisposable -Disposable $context } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - } - finally { - Update-PodeServerSignalMetric -SignalEvent $SignalEvent - Close-PodeDisposable -Disposable $context } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw $_.Exception - } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw $_.Exception + } + + # end do-while + } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active + } # start the runspace for listening on x-number of threads @@ -483,12 +478,6 @@ function Start-PodeWebServer { try { while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } Start-Sleep -Seconds 1 } } diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index 4af978667..d5aec8656 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -67,12 +67,10 @@ function Start-PodeScheduleRunspace { Start-Sleep -Seconds (60 - [DateTime]::Now.Second) while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } + + # Check for suspension or dump tokens and wait for the debugger to reset if active + Test-PodeSuspensionToken + try { $_now = [DateTime]::Now diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index b2c8fd819..f8f725dc6 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -375,12 +375,7 @@ function Restart-PodeInternalServer { $PodeContext.Server.Types = @() # recreate the session tokens - Reset-PodeCancellationToken -Type Cancellation - Reset-PodeCancellationToken -Type Restart - Reset-PodeCancellationToken -Type Dump - Reset-PodeCancellationToken -Type Suspend - Reset-PodeCancellationToken -Type Resume - Reset-PodeCancellationToken -Type Terminate + Reset-PodeCancellationToken -Type Cancellation, Restart, Dump, Suspend, Resume, Terminate # reload the configuration $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext @@ -436,14 +431,16 @@ function Reset-PodeCancellationToken { param( [Parameter(Mandatory = $true)] [validateset( 'Cancellation' , 'Restart', 'Dump', 'Suspend', 'Resume', 'Terminate' )] - [string] + [string[]] $Type ) - # Ensure cleanup of disposable tokens - Close-PodeDisposable -Disposable $PodeContext.Tokens[$Type] + $type.ForEach({ + # Ensure cleanup of disposable tokens + Close-PodeDisposable -Disposable $PodeContext.Tokens[$_] - # Reinitialize the Token - $PodeContext.Tokens[$Type] = [System.Threading.CancellationTokenSource]::new() + # Reinitialize the Token + $PodeContext.Tokens[$_] = [System.Threading.CancellationTokenSource]::new() + }) } @@ -517,36 +514,18 @@ function Suspend-PodeServerInternal { # Update the server's suspended state $PodeContext.Server.Suspended = $true - start-sleep 4 + start-sleep 2 # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) - $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` - $_.Name -notlike '*__pode_session_inmem_cleanup__*' } | Sort-Object Name + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Task*' } | Sort-Object Name foreach ($runspace in $runspaces) { - try { - # Attach debugger to the runspace - $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) - - # Enable debugging and pause execution - Enable-RunspaceDebug -BreakAll -Runspace $runspace - - # Inform user about the suspension process for the current runspace - Write-PodeHost "Waiting for $($runspace.Name) to be suspended." -NoNewLine -ForegroundColor Yellow - - # Suspend the runspace - Suspend-PodeRunspace -Runspace $Runspace - } - finally { - # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. - if ($null -ne $debugger) { - $debugger.Dispose() - } - } + # Suspend the runspace + $null = Suspend-PodeRunspace -Runspace $Runspace -NumberOfRunspaces $runspaces.Count } # Short pause before refreshing the console - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 2 # Clear the host and display header information Show-PodeConsoleInfo -ShowHeader @@ -555,10 +534,7 @@ function Suspend-PodeServerInternal { # Log any errors that occur $_ | Write-PodeErrorLog } - finally { - Reset-PodeCancellationToken -Type Suspend - #Reset-PodeCancellationToken -Type Cancellation - } + } @@ -594,7 +570,7 @@ function Resume-PodeServerInternal { Start-Sleep -Seconds 5 # Retrieve all runspaces related to Pode - $runspaces = Get-Runspace -name 'Pode_*' + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Task*' } | Sort-Object Name foreach ($runspace in $runspaces) { # Disable debugging for each runspace to restore normal execution Disable-RunspaceDebug -Runspace $runspace @@ -614,7 +590,8 @@ function Resume-PodeServerInternal { $_ | Write-PodeErrorLog } finally { + # Reinitialize the CancellationTokenSource for future suspension/resumption - Reset-PodeCancellationToken -Type Resume + Reset-PodeCancellationToken -Type Resume } } \ No newline at end of file diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index ba2e08e37..dd025c0ec 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -88,97 +88,96 @@ function Start-PodeSmtpServer { [int] $ThreadId ) + + do { + try { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + # get email + $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - # get email - $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) - - try { try { - $Request = $context.Request - $Response = $context.Response - - $script:SmtpEvent = @{ - Response = $Response - Request = $Request - Lockable = $PodeContext.Threading.Lockables.Global - Email = @{ - From = $Request.From - To = $Request.To - Data = $Request.RawBody - Headers = $Request.Headers - Subject = $Request.Subject - IsUrgent = $Request.IsUrgent - ContentType = $Request.ContentType - ContentEncoding = $Request.ContentEncoding - Attachments = $Request.Attachments - Body = $Request.Body - } - Endpoint = @{ - Protocol = $Request.Scheme - Address = $Request.Address - Name = $context.EndpointName + try { + $Request = $context.Request + $Response = $context.Response + + $script:SmtpEvent = @{ + Response = $Response + Request = $Request + Lockable = $PodeContext.Threading.Lockables.Global + Email = @{ + From = $Request.From + To = $Request.To + Data = $Request.RawBody + Headers = $Request.Headers + Subject = $Request.Subject + IsUrgent = $Request.IsUrgent + ContentType = $Request.ContentType + ContentEncoding = $Request.ContentEncoding + Attachments = $Request.Attachments + Body = $Request.Body + } + Endpoint = @{ + Protocol = $Request.Scheme + Address = $Request.Address + Name = $context.EndpointName + } + Timestamp = [datetime]::UtcNow + Metadata = @{} } - Timestamp = [datetime]::UtcNow - Metadata = @{} - } - # stop now if the request has an error - if ($Request.IsAborted) { - throw $Request.Error - } + # stop now if the request has an error + if ($Request.IsAborted) { + throw $Request.Error + } - # convert the ip - $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint) + # convert the ip + $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint) - # ensure the request ip is allowed - if (!(Test-PodeIPAccess -IP $ip)) { - $Response.WriteLine('554 Your IP address was rejected', $true) - } + # ensure the request ip is allowed + if (!(Test-PodeIPAccess -IP $ip)) { + $Response.WriteLine('554 Your IP address was rejected', $true) + } - # has the ip hit the rate limit? - elseif (!(Test-PodeIPLimit -IP $ip)) { - $Response.WriteLine('554 Your IP address has hit the rate limit', $true) - } + # has the ip hit the rate limit? + elseif (!(Test-PodeIPLimit -IP $ip)) { + $Response.WriteLine('554 Your IP address has hit the rate limit', $true) + } - # deal with smtp call - else { - $handlers = Get-PodeHandler -Type Smtp - foreach ($name in $handlers.Keys) { - $handler = $handlers[$name] - $null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat + # deal with smtp call + else { + $handlers = Get-PodeHandler -Type Smtp + foreach ($name in $handlers.Keys) { + $handler = $handlers[$name] + $null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat + } } } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + } } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException + finally { + $script:SmtpEvent = $null + Close-PodeDisposable -Disposable $context } } - finally { - $script:SmtpEvent = $null - Close-PodeDisposable -Disposable $context - } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw $_.Exception - } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw $_.Exception + } + + # end do-while + } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active + } # start the runspace for listening on x-number of threads @@ -196,12 +195,6 @@ function Start-PodeSmtpServer { try { while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } Start-Sleep -Seconds 1 } } diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index dc41f67ae..416b9f26c 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -85,118 +85,117 @@ function Start-PodeTcpServer { $ThreadId ) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - # get email - $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) + do { + try { + while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + # get email + $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) - try { try { - $Request = $context.Request - $Response = $context.Response - - $TcpEvent = @{ - Response = $Response - Request = $Request - Lockable = $PodeContext.Threading.Lockables.Global - Endpoint = @{ - Protocol = $Request.Scheme - Address = $Request.Address - Name = $context.EndpointName + try { + $Request = $context.Request + $Response = $context.Response + + $TcpEvent = @{ + Response = $Response + Request = $Request + Lockable = $PodeContext.Threading.Lockables.Global + Endpoint = @{ + Protocol = $Request.Scheme + Address = $Request.Address + Name = $context.EndpointName + } + Parameters = $null + Timestamp = [datetime]::UtcNow + Metadata = @{} } - Parameters = $null - Timestamp = [datetime]::UtcNow - Metadata = @{} - } - # stop now if the request has an error - if ($Request.IsAborted) { - throw $Request.Error - } + # stop now if the request has an error + if ($Request.IsAborted) { + throw $Request.Error + } - # convert the ip - $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint) + # convert the ip + $ip = (ConvertTo-PodeIPAddress -Address $Request.RemoteEndPoint) - # ensure the request ip is allowed - if (!(Test-PodeIPAccess -IP $ip)) { - $Response.WriteLine('Your IP address was rejected', $true) - Close-PodeTcpClient - continue - } + # ensure the request ip is allowed + if (!(Test-PodeIPAccess -IP $ip)) { + $Response.WriteLine('Your IP address was rejected', $true) + Close-PodeTcpClient + continue + } - # has the ip hit the rate limit? - if (!(Test-PodeIPLimit -IP $ip)) { - $Response.WriteLine('Your IP address has hit the rate limit', $true) - Close-PodeTcpClient - continue - } + # has the ip hit the rate limit? + if (!(Test-PodeIPLimit -IP $ip)) { + $Response.WriteLine('Your IP address has hit the rate limit', $true) + Close-PodeTcpClient + continue + } - # deal with tcp call and find the verb, and for the endpoint - if ([string]::IsNullOrEmpty($TcpEvent.Request.Body)) { - continue - } + # deal with tcp call and find the verb, and for the endpoint + if ([string]::IsNullOrEmpty($TcpEvent.Request.Body)) { + continue + } - $verb = Find-PodeVerb -Verb $TcpEvent.Request.Body -EndpointName $TcpEvent.Endpoint.Name - if ($null -eq $verb) { - $verb = Find-PodeVerb -Verb '*' -EndpointName $TcpEvent.Endpoint.Name - } + $verb = Find-PodeVerb -Verb $TcpEvent.Request.Body -EndpointName $TcpEvent.Endpoint.Name + if ($null -eq $verb) { + $verb = Find-PodeVerb -Verb '*' -EndpointName $TcpEvent.Endpoint.Name + } - if ($null -eq $verb) { - continue - } + if ($null -eq $verb) { + continue + } - # set the route parameters - if ($verb.Verb -ine '*') { - $TcpEvent.Parameters = @{} - if ($TcpEvent.Request.Body -imatch "$($verb.Verb)$") { - $TcpEvent.Parameters = $Matches + # set the route parameters + if ($verb.Verb -ine '*') { + $TcpEvent.Parameters = @{} + if ($TcpEvent.Request.Body -imatch "$($verb.Verb)$") { + $TcpEvent.Parameters = $Matches + } } - } - # invoke it - if ($null -ne $verb.Logic) { - $null = Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $verb.Arguments -UsingVariables $verb.UsingVariables -Scoped -Splat - } + # invoke it + if ($null -ne $verb.Logic) { + $null = Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $verb.Arguments -UsingVariables $verb.UsingVariables -Scoped -Splat + } - # is the verb auto-close? - if ($verb.Connection.Close) { - Close-PodeTcpClient - continue - } + # is the verb auto-close? + if ($verb.Connection.Close) { + Close-PodeTcpClient + continue + } - # is the verb auto-upgrade to ssl? - if ($verb.Connection.UpgradeToSsl) { - $Request.UpgradeToSSL() + # is the verb auto-upgrade to ssl? + if ($verb.Connection.UpgradeToSsl) { + $Request.UpgradeToSSL() + } + } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException } } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException + finally { + $TcpEvent = $null + Close-PodeDisposable -Disposable $context } } - finally { - $TcpEvent = $null - Close-PodeDisposable -Disposable $context - } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw $_.Exception - } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw $_.Exception + } + + # end do-while + } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active + } # start the runspace for listening on x-number of threads @@ -214,12 +213,6 @@ function Start-PodeTcpServer { try { while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } Start-Sleep -Seconds 1 } } diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index f6bbd032c..9c7d23867 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -22,17 +22,17 @@ function Start-PodeTimerRunspace { try { while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } + # Check for suspension or dump tokens and wait for the debugger to reset if active + Test-PodeSuspensionToken try { $_now = [DateTime]::Now # only run timers that haven't completed, and have a next trigger in the past foreach ($timer in $PodeContext.Timers.Items.Values) { + + # Check for suspension or dump tokens and wait for the debugger to reset if active + Test-PodeSuspensionToken + if ($timer.Completed -or (!$timer.OnStart -and ($timer.NextTriggerTime -gt $_now))) { continue } diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index 1d82ee09d..a4724c1aa 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -51,66 +51,63 @@ function Start-PodeWebSocketRunspace { $ThreadId ) - try { - while ($Receiver.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + do { + try { + while ($Receiver.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { + # get request + $request = (Wait-PodeTask -Task $Receiver.GetWebSocketRequestAsync($PodeContext.Tokens.Cancellation.Token)) - while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - write-podehost 'checking for PodeContext.Tokens.Dump.IsCancellationRequested' - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - # get request - $request = (Wait-PodeTask -Task $Receiver.GetWebSocketRequestAsync($PodeContext.Tokens.Cancellation.Token)) - - try { try { - $WsEvent = @{ - Request = $request - Data = $null - Files = $null - Lockable = $PodeContext.Threading.Lockables.Global - Timestamp = [datetime]::UtcNow - Metadata = @{} + try { + $WsEvent = @{ + Request = $request + Data = $null + Files = $null + Lockable = $PodeContext.Threading.Lockables.Global + Timestamp = [datetime]::UtcNow + Metadata = @{} + } + + # find the websocket definition + $websocket = Find-PodeWebSocket -Name $request.WebSocket.Name + if ($null -eq $websocket.Logic) { + continue + } + + # parse data + $result = ConvertFrom-PodeRequestContent -Request $request -ContentType $request.WebSocket.ContentType + $WsEvent.Data = $result.Data + $WsEvent.Files = $result.Files + + # invoke websocket script + $null = Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat } - - # find the websocket definition - $websocket = Find-PodeWebSocket -Name $request.WebSocket.Name - if ($null -eq $websocket.Logic) { - continue + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException } - - # parse data - $result = ConvertFrom-PodeRequestContent -Request $request -ContentType $request.WebSocket.ContentType - $WsEvent.Data = $result.Data - $WsEvent.Files = $result.Files - - # invoke websocket script - $null = Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException + finally { + $WsEvent = $null + Close-PodeDisposable -Disposable $request } } - finally { - $WsEvent = $null - Close-PodeDisposable -Disposable $request - } } - } - catch [System.OperationCanceledException] { - $_ | Write-PodeErrorLog -Level Debug - } - catch { - $_ | Write-PodeErrorLog - $_.Exception | Write-PodeErrorLog -CheckInnerException - throw $_.Exception - } + catch [System.OperationCanceledException] { + $_ | Write-PodeErrorLog -Level Debug + } + catch { + $_ | Write-PodeErrorLog + $_.Exception | Write-PodeErrorLog -CheckInnerException + throw $_.Exception + } + + # end do-while + } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active + } # start the runspace for listening on x-number of threads @@ -128,13 +125,6 @@ function Start-PodeWebSocketRunspace { try { while ($Receiver.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { - while ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } - write-podehost 'checking for PodeContext.Tokens.Dump.IsCancellationRequested' - while ($PodeContext.Tokens.Dump.IsCancellationRequested) { - Start-Sleep -Seconds 1 - } Start-Sleep -Seconds 1 } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 79563a305..400a78e5f 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -244,27 +244,13 @@ function Start-PodeServer { Clear-PodeKeyPressed Restart-PodeInternalServer } - - if (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { + elseif (($PodeContext.Tokens.Dump.IsCancellationRequested) -or (Test-PodeDumpPressed -Key $key) ) { Clear-PodeKeyPressed Invoke-PodeDump Invoke-PodeDumpInternal -Format $PodeContext.Server.Debug.Dump.Format -Path $PodeContext.Server.Debug.Dump.Path -MaxDepth $PodeContext.Server.Debug.Dump.MaxDepth } - - if (($PodeContext.Tokens.Suspend.IsCancellationRequested) -or ($PodeContext.Tokens.Resume.IsCancellationRequested) -or (Test-PodeSuspendPressed -Key $key)) { - Clear-PodeKeyPressed - if ( $PodeContext.Server.Suspended) { - Resume-PodeServer - Resume-PodeServerInternal - } - else { - Suspend-PodeServer - Suspend-PodeServerInternal - } - } - # check for open browser - if (Test-PodeOpenBrowserPressed -Key $key) { + elseif (Test-PodeOpenBrowserPressed -Key $key) { Clear-PodeKeyPressed $url = Get-PodeEndpointUrl if (![string]::IsNullOrWhitespace($url)) { @@ -272,40 +258,49 @@ function Start-PodeServer { Start-Process $url } } - - if ( Test-PodeHelpPressed -Key $key) { + elseif ( Test-PodeHelpPressed -Key $key) { Clear-PodeKeyPressed $PodeContext.Server.Console.ShowHelp = !$PodeContext.Server.Console.ShowHelp Show-PodeConsoleInfo -ShowHeader -ClearHost } - - if ( Test-PodeOpenAPIPressed -Key $key) { + elseif ( Test-PodeOpenAPIPressed -Key $key) { Clear-PodeKeyPressed $PodeContext.Server.Console.ShowOpenAPI = !$PodeContext.Server.Console.ShowOpenAPI Show-PodeConsoleInfo -ShowHeader -ClearHost } - - if ( Test-PodeEndpointsPressed -Key $key) { + elseif ( Test-PodeEndpointsPressed -Key $key) { Clear-PodeKeyPressed $PodeContext.Server.Console.ShowEndpoints = !$PodeContext.Server.Console.ShowEndpoints Show-PodeConsoleInfo -ShowHeader -ClearHost } - - if ( Test-PodeClearPressed -Key $key) { + elseif ( Test-PodeClearPressed -Key $key) { Clear-PodeKeyPressed Show-PodeConsoleInfo -ShowHeader -ClearHost } - - if ( Test-PodeQuietPressed -Key $key) { + elseif ( Test-PodeQuietPressed -Key $key) { Clear-PodeKeyPressed $PodeContext.Server.Console.Quiet = !$PodeContext.Server.Console.Quiet Show-PodeConsoleInfo -ShowHeader -ClearHost -Force } - - if ( Test-PodeTerminationPressed -Key $key) { + elseif ( Test-PodeTerminationPressed -Key $key) { Clear-PodeKeyPressed break } + elseif ( $PodeContext.Server.Suspended) { + if (($PodeContext.Tokens.Resume.IsCancellationRequested) -or (Test-PodeSuspendPressed -Key $key)) { + Clear-PodeKeyPressed + Resume-PodeServer + Resume-PodeServerInternal + } + } + else { + if ( ($PodeContext.Tokens.Suspend.IsCancellationRequested) -or (Test-PodeSuspendPressed -Key $key)) { + Clear-PodeKeyPressed + Suspend-PodeServer + Suspend-PodeServerInternal + } + } + } if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Shutdown) { @@ -405,8 +400,15 @@ function Resume-PodeServer { [CmdletBinding()] param() if ( $PodeContext.Server.Suspended) { - $PodeContext.Tokens.Resume.Cancel() - $PodeContext.Tokens.Cancellation.Cancel() + if (!$PodeContext.Tokens.Resume.IsCancellationRequested) { + $PodeContext.Tokens.Resume.Cancel() + } + if ( $PodeContext.Tokens.Cancellation.IsCancellationRequested) { + Reset-PodeCancellationToken -Type Cancellation + } + if ( $PodeContext.Tokens.Suspend.IsCancellationRequested) { + Reset-PodeCancellationToken -Type Suspend + } } } @@ -428,8 +430,12 @@ function Suspend-PodeServer { [CmdletBinding()] param() if (! $PodeContext.Server.Suspended) { - $PodeContext.Tokens.Cancellation.Cancel() - $PodeContext.Tokens.Suspend.Cancel() + if (!$PodeContext.Tokens.Suspend.IsCancellationRequested) { + $PodeContext.Tokens.Suspend.Cancel() + } + if (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + $PodeContext.Tokens.Cancellation.Cancel() + } } } From 1791f76753d4ea6f9b3eac360a0863847eec0822 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 6 Dec 2024 10:58:25 -0800 Subject: [PATCH 30/34] Add progress bar --- src/Private/Dump.ps1 | 89 +++++++++++++++++++++++++++++------------- src/Private/Server.ps1 | 19 +++++++-- 2 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index a9719750d..e1f0e3a17 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -162,13 +162,29 @@ function Invoke-PodeDumpInternal { } | Sort-Object Name + $runspaceDetails = @{} + # Initialize the master progress bar + $runspaceCount = $runspaces.Count + $currentRunspaceIndex = 0 + $masterActivityId = (Get-Random) # Unique ID for master progress + foreach ($r in $runspaces) { + # Update master progress bar + $currentRunspaceIndex++ + $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) + + Write-Progress -Activity 'Suspending Runspaces' ` + -Status "Processing runspace $($currentRunspaceIndex) of $($runspaceCount): $($r.Name)" ` + -PercentComplete $masterPercentComplete ` + -Id $masterActivityId + + # Call Suspend-PodeRunspace with nested child progress bar $runspaceDetails[$r.Name] = @{ Id = $r.Id Name = @{ $r.Name = @{ - ScopedVariables = Suspend-PodeRunspace -Runspace $r -NumberOfRunspaces $runspaces.Count + ScopedVariables = Suspend-PodeRunspace -Runspace $r -ParentActivityId $masterActivityId -CollectVariable } } InitialSessionState = $r.InitialSessionState @@ -176,6 +192,10 @@ function Invoke-PodeDumpInternal { } } + # Clear master progress bar once all runspaces are processed + Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $masterActivityId + + if ($null -ne $PodeContext.RunspacePools) { foreach ($poolName in $PodeContext.RunspacePools.Keys) { $pool = $PodeContext.RunspacePools[$poolName] @@ -258,9 +278,6 @@ function Invoke-PodeDumpInternal { .PARAMETER Timeout The maximum time (in seconds) to wait for the debugger stop event to be triggered. Defaults to 60 seconds. -.PARAMETER NumberOfRunspaces - The total numebr of Runspaces to collect. - .EXAMPLE $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $runspace.Open() @@ -281,40 +298,57 @@ function Suspend-PodeRunspace { [Parameter()] [int] - $Timeout = 60, + $Timeout = 30, - [Parameter(Mandatory = $true)] + [Parameter()] [int] - $NumberOfRunspaces, + $ParentActivityId, [switch] $CollectVariable - ) - try { - - # Wait for the event to be triggered or timeout - Write-PodeHost "Waiting for $($Runspace.Name) to be suspended." -NoNewLine + try { + # Initialize debugger $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) Enable-RunspaceDebug -BreakAll -Runspace $Runspace - # Wait for the event to be triggered or timeout + # Initialize progress bar variables $startTime = [DateTime]::UtcNow - Start-Sleep -Milliseconds 500 + $elapsedTime = 0 + $childActivityId = (Get-Random) # Unique ID for child progress bar - Write-PodeHost '.' -NoNewLine + # Initial progress bar display + Write-Progress -Activity "Suspending Runspace $($Runspace.Name)" ` + -Status 'Waiting for suspension...' ` + -PercentComplete 0 ` + -Id $childActivityId ` + -ParentId $ParentActivityId while (!$debugger.IsEventTriggered) { - Start-Sleep -Milliseconds 1000 - Write-PodeHost '.' -NoNewLine - if (([DateTime]::UtcNow - $startTime).TotalSeconds -ge $Timeout) { + # Update elapsed time and progress + $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds + $percentComplete = [math]::Min(($elapsedTime / $Timeout) * 100, 100) + + Write-Progress -Activity "Suspending Runspace $($Runspace.Name)" ` + -Status 'Waiting for suspension...' ` + -PercentComplete $percentComplete ` + -Id $childActivityId ` + -ParentId $ParentActivityId + + # Check for timeout + if ($elapsedTime -ge $Timeout) { + Write-Progress -Completed -Id $childActivityId Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" - if ( $CollectVariable) { return @{} }else { return $false } + if ($CollectVariable) { return @{} } else { return $false } } + + Start-Sleep -Milliseconds 1000 } - Write-PodeHost 'Done' - if ( $CollectVariable) { + + # Completion message + Write-Progress -Completed -Id $childActivityId + if ($CollectVariable) { # Return the collected variables return $debugger.Variables } @@ -323,23 +357,22 @@ function Suspend-PodeRunspace { } } catch { - # Log the error details using Write-PodeErrorLog. - # This ensures that any exceptions thrown during the execution are logged appropriately. + # Log the error details using Write-PodeErrorLog for troubleshooting. $_ | Write-PodeErrorLog } finally { - # Detach the debugger from the runspace to clean up resources and prevent any lingering event handlers. + # Clean up debugger resources and disable debugging if ($null -ne $debugger) { $debugger.Dispose() } if ($CollectVariable) { - # Disable debugging for the runspace. This ensures that the runspace returns to its normal execution state. Disable-RunspaceDebug -Runspace $Runspace } } - if ( $CollectVariable) { - return @{} + # Fallback returns for unhandled scenarios + if ($CollectVariable) { + return @{ } } else { return $false @@ -347,6 +380,8 @@ function Suspend-PodeRunspace { } + + <# .SYNOPSIS Collects and serializes variables from different scopes (Local, Script, Global). diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index f8f725dc6..7c17f630c 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -518,11 +518,24 @@ function Suspend-PodeServerInternal { # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Task*' } | Sort-Object Name - - foreach ($runspace in $runspaces) { + # Initialize the master progress bar + $runspaceCount = $runspaces.Count + $currentRunspaceIndex = 0 + $masterActivityId = (Get-Random) # Unique ID for master progress + foreach ($r in $runspaces) { + # Update master progress bar + $currentRunspaceIndex++ + $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) + + Write-Progress -Activity 'Suspending Runspaces' ` + -Status "Processing runspace $($currentRunspaceIndex) of $($runspaceCount): $($r.Name)" ` + -PercentComplete $masterPercentComplete ` + -Id $masterActivityId # Suspend the runspace - $null = Suspend-PodeRunspace -Runspace $Runspace -NumberOfRunspaces $runspaces.Count + $null = Suspend-PodeRunspace -Runspace $r -ParentActivityId $masterActivityId } + # Clear master progress bar once all runspaces are processed + Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $masterActivityId # Short pause before refreshing the console Start-Sleep -Seconds 2 From 525638f0618b3f7adedc85fdf6a319082dfb32d7 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 6 Dec 2024 16:58:42 -0800 Subject: [PATCH 31/34] debugging --- src/Embedded/DebuggerHandler.cs | 2 +- src/Pode.psd1 | 1 + src/Private/Dump.ps1 | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Embedded/DebuggerHandler.cs b/src/Embedded/DebuggerHandler.cs index 96257e12a..e16f3e60c 100644 --- a/src/Embedded/DebuggerHandler.cs +++ b/src/Embedded/DebuggerHandler.cs @@ -79,7 +79,7 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) var command = new PSCommand(); command.AddCommand(_collectVariables ? "Get-PodeDumpScopedVariable" - : "while($PodeContext.Server.Suspended) { Start-Sleep -Milliseconds 500 }"); + : "while($PodeContext.Tokens.Suspend.IsCancellationRequested) { Start-Sleep -Milliseconds 500 }; Continue"); // Execute the command within the debugger var outputCollection = new PSDataCollection(); diff --git a/src/Pode.psd1 b/src/Pode.psd1 index caf67a0c0..d024b2790 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -145,6 +145,7 @@ 'Set-PodeCurrentRunspaceName', 'Invoke-PodeGC', 'Invoke-PodeDump', + 'Start-PodeSleep', # routes 'Add-PodeRoute', diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index e1f0e3a17..806d17a60 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -194,6 +194,9 @@ function Invoke-PodeDumpInternal { # Clear master progress bar once all runspaces are processed Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $masterActivityId + # $outputCollection = [System.Management.Automation.PSDataCollection[psobject]]::new() + # $cmd=[System.Management.Automation.PSCommand]::new().AddCommand('Continue') + # $p.Debugger.ProcessCommand($cmd,$outputCollection) if ($null -ne $PodeContext.RunspacePools) { @@ -256,6 +259,8 @@ function Invoke-PodeDumpInternal { } Write-PodeHost -ForegroundColor Yellow "Memory dump saved to $dumpFilePath" + + Get-Runspace } end { @@ -361,10 +366,13 @@ function Suspend-PodeRunspace { $_ | Write-PodeErrorLog } finally { + Start-Sleep 1 # Clean up debugger resources and disable debugging if ($null -ne $debugger) { + $debugger.Dispose() } + if ($CollectVariable) { Disable-RunspaceDebug -Runspace $Runspace } From 5493c919fbcbdf6b0954abc97d0d74ebddcd4d13 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 7 Dec 2024 14:15:51 -0800 Subject: [PATCH 32/34] suspend resume fixed --- examples/Web-Dump.ps1 | 69 +++++++++++++++++---- src/Embedded/DebuggerHandler.cs | 26 ++++---- src/Private/Dump.ps1 | 81 ++++++++++++++---------- src/Private/Runspaces.ps1 | 43 +++++++++++-- src/Private/Schedules.ps1 | 6 +- src/Private/Server.ps1 | 105 ++++++++++++++++++++++++-------- src/Private/Tasks.ps1 | 1 + src/Public/Core.ps1 | 10 +++ src/Public/Utilities.ps1 | 99 ++++++++++++++++++++++++++---- 9 files changed, 336 insertions(+), 104 deletions(-) diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 index b4067de13..cd666d8df 100644 --- a/examples/Web-Dump.ps1 +++ b/examples/Web-Dump.ps1 @@ -51,6 +51,8 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { Add-PodeEndpoint -Address localhost -Port 8100 -Protocol Tcp Add-PodeEndpoint -Address localhost -Port 9002 -Protocol Tcps -SelfSigned + Set-PodeTaskConcurrency -Maximum 10 + # set view engine to pode renderer Set-PodeViewEngine -Type Html @@ -103,7 +105,7 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { Add-PodeVerb -Verb 'HELLO' -ScriptBlock { Write-PodeTcpClient -Message 'HI' - 'here' | Out-Default + 'here' } # setup an smtp handler @@ -116,14 +118,14 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { Write-PodeHost '|' # Write-PodeHost $SmtpEvent.Email.Data # Write-PodeHost '|' - $SmtpEvent.Email.Attachments | Out-Default + $SmtpEvent.Email.Attachments if ($SmtpEvent.Email.Attachments.Length -gt 0) { #$SmtpEvent.Email.Attachments[0].Save('C:\temp') } Write-PodeHost '|' - $SmtpEvent.Email | Out-Default - $SmtpEvent.Request | out-default - $SmtpEvent.Email.Headers | out-default + $SmtpEvent.Email + $SmtpEvent.Request + $SmtpEvent.Email.Headers Write-PodeHost '- - - - - - - - - - - - - - - - - -' } @@ -158,20 +160,61 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { Add-PodeTask -Name 'Test' -ScriptBlock { param($value) Start-PodeSleep -Seconds 10 - "a $($value) is comming" | Out-Default + write-podehost "a $($value) is comming" + Start-PodeSleep -Seconds 10 + write-podehost "a $($value) is comming...2" Start-PodeSleep -Seconds 10 - "a $($value) is comming...2" | Out-Default + write-podehost "a $($value) is comming...3" Start-PodeSleep -Seconds 10 - "a $($value) is comming...3" | Out-Default + write-podehost "a $($value) is comming...4" Start-PodeSleep -Seconds 10 - "a $($value) is comming...4" | Out-Default + write-podehost "a $($value) is comming...5" Start-PodeSleep -Seconds 10 - "a $($value) is comming...5" | Out-Default + write-podehost "a $($value) is comming...6" Start-PodeSleep -Seconds 10 - "a $($value) is comming...6" | Out-Default - Start-PodeSleep -Seconds 100 - "a $($value) is never late, it arrives exactly when it means to" | Out-Default + write-podehost "a $($value) is never late, it arrives exactly when it means to" + } + # schedule minutely using predefined cron + $message = 'Hello, world!' + Add-PodeSchedule -Name 'predefined' -Cron '@minutely' -Limit 2 -ScriptBlock { + param($Event, $Message1, $Message2) + $using:message | Out-Default + Get-PodeSchedule -Name 'predefined' | Out-Default + "Last: $($Event.Sender.LastTriggerTime)" | Out-Default + "Next: $($Event.Sender.NextTriggerTime)" | Out-Default + "Message1: $($Message1)" | Out-Default + "Message2: $($Message2)" | Out-Default + } + + Add-PodeSchedule -Name 'from-file' -Cron '@minutely' -FilePath './scripts/schedule.ps1' + + # schedule defined using two cron expressions + Add-PodeSchedule -Name 'two-crons' -Cron @('0/3 * * * *', '0/5 * * * *') -ScriptBlock { + 'double cron' | Out-Default + Get-PodeSchedule -Name 'two-crons' | Out-Default + } + + # schedule to run every tuesday at midnight + Add-PodeSchedule -Name 'tuesdays' -Cron '0 0 * * TUE' -ScriptBlock { + # logic } + # schedule to run every 5 past the hour, starting in 2hrs + Add-PodeSchedule -Name 'hourly-start' -Cron '5,7,9 * * * *' -ScriptBlock { + # logic + } -StartTime ([DateTime]::Now.AddHours(2)) + + # schedule to run every 10 minutes, and end in 2hrs + Add-PodeSchedule -Name 'every-10mins-end' -Cron '0/10 * * * *' -ScriptBlock { + # logic + } -EndTime ([DateTime]::Now.AddHours(2)) + + # adhoc invoke a schedule's logic + Add-PodeRoute -Method Get -Path '/api/run' -ScriptBlock { + Invoke-PodeSchedule -Name 'predefined' -ArgumentList @{ + Message1 = 'Hello!' + Message2 = 'Bye!' + } + } } \ No newline at end of file diff --git a/src/Embedded/DebuggerHandler.cs b/src/Embedded/DebuggerHandler.cs index e16f3e60c..c0ca7023e 100644 --- a/src/Embedded/DebuggerHandler.cs +++ b/src/Embedded/DebuggerHandler.cs @@ -71,23 +71,19 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) { return; } - - // Enable step mode for command execution - debugger.SetDebuggerStepMode(true); - - // Create the command to execute - var command = new PSCommand(); - command.AddCommand(_collectVariables - ? "Get-PodeDumpScopedVariable" - : "while($PodeContext.Tokens.Suspend.IsCancellationRequested) { Start-Sleep -Milliseconds 500 }; Continue"); - - // Execute the command within the debugger - var outputCollection = new PSDataCollection(); - debugger.ProcessCommand(command, outputCollection); - - // Collect the variables if required if (_collectVariables) { + // Enable step mode for command execution + debugger.SetDebuggerStepMode(true); + + // Create the command to execute + var command = new PSCommand(); + command.AddCommand("Get-PodeDumpScopedVariable"); + // Execute the command within the debugger + var outputCollection = new PSDataCollection(); + debugger.ProcessCommand(command, outputCollection); + + // Collect the variables if required foreach (var output in outputCollection) { Variables.Add(output); diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index 806d17a60..eeeb21fc9 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -165,36 +165,46 @@ function Invoke-PodeDumpInternal { $runspaceDetails = @{} # Initialize the master progress bar - $runspaceCount = $runspaces.Count + $runspaceCount = $runspaces.Count + 1 $currentRunspaceIndex = 0 - $masterActivityId = (Get-Random) # Unique ID for master progress - - foreach ($r in $runspaces) { - # Update master progress bar - $currentRunspaceIndex++ - $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) - - Write-Progress -Activity 'Suspending Runspaces' ` - -Status "Processing runspace $($currentRunspaceIndex) of $($runspaceCount): $($r.Name)" ` - -PercentComplete $masterPercentComplete ` - -Id $masterActivityId - - # Call Suspend-PodeRunspace with nested child progress bar - $runspaceDetails[$r.Name] = @{ - Id = $r.Id - Name = @{ - $r.Name = @{ - ScopedVariables = Suspend-PodeRunspace -Runspace $r -ParentActivityId $masterActivityId -CollectVariable + $skipped = 0 + $mainActivityId = (Get-Random) # Unique ID for master progress + try { + foreach ($r in $runspaces) { + try { + # Update master progress bar + $currentRunspaceIndex++ + $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) + + Write-Progress -Activity 'Suspending Runspaces' ` + -Status "Processing runspace $($currentRunspaceIndex) of $($runspaceCount): $($r.Name)" ` + -PercentComplete $masterPercentComplete ` + -Id $mainActivityId + + # Call Suspend-PodeRunspace with nested child progress bar + $runspaceDetails[$r.Name] = @{ + Id = $r.Id + Name = @{ + $r.Name = @{ + ScopedVariables = Suspend-PodeRunspace -Runspace $r -ParentActivityId $mainActivityId -CollectVariable + } + } + InitialSessionState = $r.InitialSessionState + RunspaceStateInfo = $r.RunspaceStateInfo } } - InitialSessionState = $r.InitialSessionState - RunspaceStateInfo = $r.RunspaceStateInfo + catch { + $_ | Write-PodeErrorLog + } } } - - # Clear master progress bar once all runspaces are processed - Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $masterActivityId - # $outputCollection = [System.Management.Automation.PSDataCollection[psobject]]::new() + finally { + Start-PodeSleep -Seconds 3 -ParentId $mainActivityId -ShowProgress + # Clear master progress bar once all runspaces are processed + Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $mainActivityId + Write-Verbose "$skipped of $runspaceCount runspaces are not in a busy state" + } + # $outputCollection = [System.Management.Automation.PSDataCollection[psobject]]::new() # $cmd=[System.Management.Automation.PSCommand]::new().AddCommand('Continue') # $p.Debugger.ProcessCommand($cmd,$outputCollection) @@ -343,19 +353,26 @@ function Suspend-PodeRunspace { # Check for timeout if ($elapsedTime -ge $Timeout) { - Write-Progress -Completed -Id $childActivityId - Write-PodeHost "Failed (Timeout reached after $Timeout seconds.)" + Write-Progress -Completed -Id $childActivityId -ParentId $ParentActivityId + Write-PodeHost "$($Runspace.Name) failed (Timeout reached after $Timeout seconds.)" if ($CollectVariable) { return @{} } else { return $false } } Start-Sleep -Milliseconds 1000 } - - # Completion message - Write-Progress -Completed -Id $childActivityId if ($CollectVariable) { + $r = $debugger.Variables + <# $variables = [System.Management.Automation.PSDataCollection[psobject]]::new(); + $outputCollection = [System.Management.Automation.PSDataCollection[psobject]]::new() + $cmd = [System.Management.Automation.PSCommand]::new().AddCommand('Get-PodeDumpScopedVariable') + $Runspace.Debugger.ProcessCommand($cmd, $outputCollection) + + foreach ($output in $outputCollection) + { + $variables.Add($output) + }#> # Return the collected variables - return $debugger.Variables + return $r } else { return $true @@ -376,6 +393,8 @@ function Suspend-PodeRunspace { if ($CollectVariable) { Disable-RunspaceDebug -Runspace $Runspace } + # Completion message + Write-Progress -Completed -Id $childActivityId } # Fallback returns for unhandled scenarios diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index 7bedb2f33..a58025664 100644 --- a/src/Private/Runspaces.ps1 +++ b/src/Private/Runspaces.ps1 @@ -304,18 +304,53 @@ function Close-PodeRunspace { } } + <# +.SYNOPSIS + Resets the name of the current Pode runspace by modifying its structure. +.DESCRIPTION + The `Reset-PodeRunspaceName` function updates the name of the current runspace if it begins with "Pode_". + It replaces the portion of the name after the second underscore with "waiting" while retaining the final number. + Additionally, it prepends an underscore (`_`) to the modified name. +.PARAMETER None + This function does not take any parameters. +.NOTES + - The function assumes the current runspace follows the naming convention "Pode_*". + - If the current runspace name does not start with "Pode_", no changes are made. + - Useful for managing or resetting runspace names in Pode applications. +.EXAMPLE + # Example 1: Current runspace name is Pode_Tasks_Test_1 + Reset-PodeRunspaceName + # After execution: Runspace name becomes _Pode_Tasks_waiting_1 + # Example 2: Current runspace name is NotPode_Runspace + Reset-PodeRunspaceName + # No changes are made because the name does not start with "Pode_". +.EXAMPLE + # Example 3: Runspace with custom name + Reset-PodeRunspaceName + # Before: Pode_CustomRoute_Process_5 + # After: _Pode_CustomRoute_waiting_5 +.OUTPUTS + None. +#> +function Reset-PodeRunspaceName { + [CmdletBinding()] + # Get the current runspace + $currentRunspace = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace + # Check if the runspace name starts with 'Pode_' + if (-not $currentRunspace.Name.StartsWith('Pode_')) { + return + } - - - - + # Update the runspace name with the required format + $currentRunspace.Name = "_$($currentRunspace.Name -replace '(^[^_]*_[^_]*_)[^_]*_(\d+)$', '${1}waiting_${2}')" +} diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index d5aec8656..765a06e0b 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -64,10 +64,10 @@ function Start-PodeScheduleRunspace { Complete-PodeInternalSchedule -Now $_now # first, sleep for a period of time to get to 00 seconds (start of minute) - Start-Sleep -Seconds (60 - [DateTime]::Now.Second) + Start-PodeSleep -Seconds (60 - [DateTime]::Now.Second) while (!$PodeContext.Tokens.Terminate.IsCancellationRequested) { - + # Check for suspension or dump tokens and wait for the debugger to reset if active Test-PodeSuspensionToken @@ -99,7 +99,6 @@ function Start-PodeScheduleRunspace { # Loop in 5-second intervals until the remaining seconds are covered while ($remainingSeconds -gt 0) { $sleepTime = [math]::Min(5, $remainingSeconds) # Sleep for 5 seconds or remaining time - # Start-Sleep -Seconds $sleepTime Start-PodeSleep -Seconds $sleepTime $remainingSeconds -= $sleepTime } @@ -337,6 +336,7 @@ function Get-PodeScheduleScriptBlock { $_ | Write-PodeErrorLog } finally { + Reset-PodeRunspaceName Invoke-PodeGC } } diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 7c17f630c..e68ec7ff3 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -515,30 +515,85 @@ function Suspend-PodeServerInternal { # Update the server's suspended state $PodeContext.Server.Suspended = $true start-sleep 2 + try { + # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Tasks_*' -or $_.Name -like 'Pode_Schedules_*' } | Sort-Object Name + # Initialize the master progress bar + $runspaceCount = $runspaces.Count + $currentRunspaceIndex = 0 + $masterActivityId = (Get-Random) # Unique ID for master progress + $runspaces | Foreach-Object { + + $originalName = $_.Name + # Initialize progress bar variables + $startTime = [DateTime]::UtcNow + $elapsedTime = 0 + $childActivityId = (Get-Random) # Unique ID for child progress bar + # Update master progress bar + $currentRunspaceIndex++ + $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) + + Write-Progress -Activity 'Suspending Runspaces' ` + -Status "Processing runspace $($currentRunspaceIndex) of $($runspaceCount): $($_.Name)" ` + -PercentComplete $masterPercentComplete ` + -Id $masterActivityId + # Suspend the runspace + Enable-RunspaceDebug -BreakAll -Runspace $_ + try { + # Initial progress bar display + Write-Progress -Activity "Suspending Runspace $($_.Name)" ` + -Status 'Waiting for suspension...' ` + -PercentComplete 0 ` + -Id $childActivityId ` + -ParentId $masterActivityId + + while (! $_.debugger.InBreakpoint) { + # Update elapsed time and progress + $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds + $percentComplete = [math]::Min(($elapsedTime / $Timeout) * 100, 100) + + Write-Progress -Activity "Suspending Runspace $($_.Name)" ` + -Status 'Waiting for suspension...' ` + -PercentComplete $percentComplete ` + -Id $childActivityId ` + -ParentId $masterActivityId + + if ($_.Name.StartsWith('_')) { + Write-PodeHost "$originalName runspace has beed completed" + Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + break + } + # Check for timeout + if ($elapsedTime -ge $Timeout) { + Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + Write-PodeHost "$($_.Name) failed (Timeout reached after $Timeout seconds.)" + break + } + + Start-Sleep -Milliseconds 1000 + } - # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) - $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Task*' } | Sort-Object Name - # Initialize the master progress bar - $runspaceCount = $runspaces.Count - $currentRunspaceIndex = 0 - $masterActivityId = (Get-Random) # Unique ID for master progress - foreach ($r in $runspaces) { - # Update master progress bar - $currentRunspaceIndex++ - $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) - - Write-Progress -Activity 'Suspending Runspaces' ` - -Status "Processing runspace $($currentRunspaceIndex) of $($runspaceCount): $($r.Name)" ` - -PercentComplete $masterPercentComplete ` - -Id $masterActivityId - # Suspend the runspace - $null = Suspend-PodeRunspace -Runspace $r -ParentActivityId $masterActivityId + } + catch { + $_ | Write-PodeErrorLog + } + finally { + # Completion message + Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + Start-Sleep -Milliseconds 1000 + } + } + } + catch { + $_ | Write-PodeErrorLog + } + finally { + # Clear master progress bar once all runspaces are processed + Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $masterActivityId } - # Clear master progress bar once all runspaces are processed - Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $masterActivityId # Short pause before refreshing the console - Start-Sleep -Seconds 2 + Start-Sleep -Seconds 1 # Clear the host and display header information Show-PodeConsoleInfo -ShowHeader @@ -580,14 +635,10 @@ function Resume-PodeServerInternal { $PodeContext.Server.Suspended = $false # Suspend briefly to ensure any required internal processes have time to stabilize - Start-Sleep -Seconds 5 + Start-Sleep -Seconds 1 - # Retrieve all runspaces related to Pode - $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Task*' } | Sort-Object Name - foreach ($runspace in $runspaces) { - # Disable debugging for each runspace to restore normal execution - Disable-RunspaceDebug -Runspace $runspace - } + # Disable debugging for each runspace to restore normal execution + Get-Runspace | Where-Object { $_.Debugger.InBreakpoint } | Disable-RunspaceDebug # Inform user that the resume process is complete Write-PodeHost 'Done' -ForegroundColor Green diff --git a/src/Private/Tasks.ps1 b/src/Private/Tasks.ps1 index b723a4f74..b4a966ac4 100644 --- a/src/Private/Tasks.ps1 +++ b/src/Private/Tasks.ps1 @@ -211,6 +211,7 @@ function Get-PodeTaskScriptBlock { $_ | Write-PodeErrorLog } finally { + Reset-PodeRunspaceName Invoke-PodeGC } } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 400a78e5f..b4c8b8e00 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -208,6 +208,16 @@ function Start-PodeServer { EnableBreakpoints = $EnableBreakpoints } + # Determines how PowerShell responds to progress updates generated by a script, cmdlet, or provider, + # such as the progress bars generated by the Write-Progress cmdlet. + # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables?view=powershell-7.4 + if ($Quiet) { + $ProgressPreference = 'SilentlyContinue' + } + else { + $ProgressPreference = 'Continue' + } + # Create main context object $PodeContext = New-PodeContext @ContextParams diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index f89340c58..810414d85 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1541,7 +1541,7 @@ function Invoke-PodeDump { $ErrorRecord, [Parameter()] - [ValidateSet('JSON', 'CLIXML', 'TXT', 'BIN', 'YAML')] + [ValidateSet('JSON', 'CLIXML', 'TXT', 'BIN', 'YAML')] [string] $Format, @@ -1557,7 +1557,54 @@ function Invoke-PodeDump { } +<# +.SYNOPSIS + A function to pause execution for a specified duration with an optional progress bar. + +.DESCRIPTION + The `Start-PodeSleep` function pauses script execution for a given duration specified in seconds, milliseconds, or a TimeSpan. + It includes an optional progress bar that displays the elapsed time and completion percentage. + The progress bar can also display a custom activity name and allows grouping with a ParentId. + +.PARAMETER Seconds + Specifies the duration to pause execution in seconds. Default is 1 second. + +.PARAMETER Milliseconds + Specifies the duration to pause execution in milliseconds. + +.PARAMETER Duration + Specifies the duration to pause execution using a TimeSpan object. + +.PARAMETER Activity + Specifies the activity name displayed in the progress bar. Default is "Sleeping...". + +.PARAMETER ParentId + Optional parameter to specify the ParentId for the progress bar, enabling hierarchical grouping. + +.PARAMETER ShowProgress + Switch to enable the progress bar during the sleep duration. + +.OUTPUTS + None. + +.EXAMPLE + Start-PodeSleep -Seconds 5 -ShowProgress + + Pauses execution for 5 seconds and displays a progress bar. + +.EXAMPLE + Start-PodeSleep -Milliseconds 3000 -ShowProgress -Activity "Processing Task" -ParentId 1 + + Pauses execution for 3000 milliseconds, showing a progress bar with the custom activity grouped under ParentId 1. +.EXAMPLE + Start-PodeSleep -Duration (New-TimeSpan -Seconds 10) -ShowProgress -Activity "Running Script" + + Pauses execution for 10 seconds using a TimeSpan object and displays a progress bar. + +.NOTES + This function is useful for scenarios where tracking the remaining wait time visually is helpful. +#> function Start-PodeSleep { [CmdletBinding()] param ( @@ -1568,30 +1615,60 @@ function Start-PodeSleep { [int]$Milliseconds, [Parameter(Position = 0, Mandatory = $false, ParameterSetName = 'Duration')] - [TimeSpan]$Duration + [TimeSpan]$Duration, + + [Parameter(Position = 1, Mandatory = $false)] + [string]$Activity = "Sleeping...", + + [Parameter(Mandatory = $false)] + [int]$ParentId, + + [Parameter(Mandatory = $false)] + [Switch]$ShowProgress ) - # Determine end time based on the parameter set + # Determine total duration and end time switch ($PSCmdlet.ParameterSetName) { 'Seconds' { - $endTime = (Get-Date).AddSeconds($Seconds) + $totalDuration = [TimeSpan]::FromSeconds($Seconds) } 'Milliseconds' { - $endTime = (Get-Date).AddMilliseconds($Milliseconds) + $totalDuration = [TimeSpan]::FromMilliseconds($Milliseconds) } 'Duration' { - $endTime = (Get-Date).Add($Duration) + $totalDuration = $Duration } } + $endTime = (Get-Date).Add($totalDuration) + + # Start the timer + $startTime = Get-Date while ((Get-Date) -lt $endTime) { - # Check if a debugger is attached - # if ($Host.Debugger.IsActive) { - # Write-PodeHost "Debugger is attached. Waiting for interaction..." - # Debugger # Trigger a breakpoint to allow interaction - # } + if ($ShowProgress) { + # Calculate progress and build Write-Progress parameters + $progressParams = @{ + Activity = $Activity + Status = "$([math]::Round((($(Get-Date) - $startTime).TotalMilliseconds / $totalDuration.TotalMilliseconds) * 100, 1))%" + PercentComplete = [math]::Min((($(Get-Date) - $startTime).TotalMilliseconds / $totalDuration.TotalMilliseconds) * 100, 100) + } + if ($ParentId) { + $progressParams.ParentId = $ParentId + } + + # Write the progress with dynamic parameters + Write-Progress @progressParams + } # Sleep for a short duration to prevent high CPU usage Start-Sleep -Milliseconds 200 } + + # Clear the progress bar after completion + if ($ShowProgress) { + Write-Progress -Activity $Activity -Completed + } } + + + From 3ea0267850405119a7fc7ecbd482d566c913d666 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 7 Dec 2024 19:03:55 -0800 Subject: [PATCH 33/34] fixes --- examples/Web-Dump.ps1 | 48 ++++----- src/Locales/ar/Pode.psd1 | 4 + src/Locales/de/Pode.psd1 | 4 + src/Locales/en-us/Pode.psd1 | 4 + src/Locales/en/Pode.psd1 | 4 + src/Locales/es/Pode.psd1 | 4 + src/Locales/fr/Pode.psd1 | 4 + src/Locales/it/Pode.psd1 | 4 + src/Locales/ja/Pode.psd1 | 4 + src/Locales/ko/Pode.psd1 | 4 + src/Locales/nl/Pode.psd1 | 4 + src/Locales/pl/Pode.psd1 | 4 + src/Locales/pt/Pode.psd1 | 4 + src/Locales/zh/Pode.psd1 | 4 + src/Private/Server.ps1 | 198 ++++++++++++++++++++++++++++++++---- 15 files changed, 252 insertions(+), 46 deletions(-) diff --git a/examples/Web-Dump.ps1 b/examples/Web-Dump.ps1 index cd666d8df..87def166b 100644 --- a/examples/Web-Dump.ps1 +++ b/examples/Web-Dump.ps1 @@ -110,23 +110,23 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { # setup an smtp handler Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { - Write-PodeHost '- - - - - - - - - - - - - - - - - -' - Write-PodeHost $SmtpEvent.Email.From - Write-PodeHost $SmtpEvent.Email.To - Write-PodeHost '|' - Write-PodeHost $SmtpEvent.Email.Body - Write-PodeHost '|' - # Write-PodeHost $SmtpEvent.Email.Data - # Write-PodeHost '|' + Write-Verbose '- - - - - - - - - - - - - - - - - -' + Write-Verbose $SmtpEvent.Email.From + Write-Verbose $SmtpEvent.Email.To + Write-Verbose '|' + Write-Verbose $SmtpEvent.Email.Body + Write-Verbose '|' + # Write-Verbose $SmtpEvent.Email.Data + # Write-Verbose '|' $SmtpEvent.Email.Attachments if ($SmtpEvent.Email.Attachments.Length -gt 0) { #$SmtpEvent.Email.Attachments[0].Save('C:\temp') } - Write-PodeHost '|' + Write-Verbose '|' $SmtpEvent.Email $SmtpEvent.Request $SmtpEvent.Email.Headers - Write-PodeHost '- - - - - - - - - - - - - - - - - -' + Write-Verbose '- - - - - - - - - - - - - - - - - -' } # GET request for web page @@ -160,37 +160,37 @@ Start-PodeServer -Threads 4 -EnablePool Tasks -ScriptBlock { Add-PodeTask -Name 'Test' -ScriptBlock { param($value) Start-PodeSleep -Seconds 10 - write-podehost "a $($value) is comming" + Write-Verbose "a $($value) is comming" Start-PodeSleep -Seconds 10 - write-podehost "a $($value) is comming...2" + Write-Verbose "a $($value) is comming...2" Start-PodeSleep -Seconds 10 - write-podehost "a $($value) is comming...3" + Write-Verbose "a $($value) is comming...3" Start-PodeSleep -Seconds 10 - write-podehost "a $($value) is comming...4" + Write-Verbose "a $($value) is comming...4" Start-PodeSleep -Seconds 10 - write-podehost "a $($value) is comming...5" + Write-Verbose "a $($value) is comming...5" Start-PodeSleep -Seconds 10 - write-podehost "a $($value) is comming...6" + Write-Verbose "a $($value) is comming...6" Start-PodeSleep -Seconds 10 - write-podehost "a $($value) is never late, it arrives exactly when it means to" + Write-Verbose "a $($value) is never late, it arrives exactly when it means to" } # schedule minutely using predefined cron $message = 'Hello, world!' Add-PodeSchedule -Name 'predefined' -Cron '@minutely' -Limit 2 -ScriptBlock { param($Event, $Message1, $Message2) - $using:message | Out-Default - Get-PodeSchedule -Name 'predefined' | Out-Default - "Last: $($Event.Sender.LastTriggerTime)" | Out-Default - "Next: $($Event.Sender.NextTriggerTime)" | Out-Default - "Message1: $($Message1)" | Out-Default - "Message2: $($Message2)" | Out-Default + Write-Verbose $using:message + Get-PodeSchedule -Name 'predefined' + Write-Verbose "Last: $($Event.Sender.LastTriggerTime)" + Write-Verbose "Next: $($Event.Sender.NextTriggerTime)" + Write-Verbose "Message1: $($Message1)" + Write-Verbose "Message2: $($Message2)" } Add-PodeSchedule -Name 'from-file' -Cron '@minutely' -FilePath './scripts/schedule.ps1' # schedule defined using two cron expressions Add-PodeSchedule -Name 'two-crons' -Cron @('0/3 * * * *', '0/5 * * * *') -ScriptBlock { - 'double cron' | Out-Default + Write-Verbose 'double cron' Get-PodeSchedule -Name 'two-crons' | Out-Default } diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index c84b5aef1..0f2cd847a 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'معلق' runningMessage = 'يعمل' openHttpEndpointMessage = 'افتح أول نقطة نهاية HTTP في المتصفح الافتراضي.' + suspendingRunspaceMessage = 'إيقاف تنفيذ المسارات' + resumingRunspaceMessage = 'استئناف تنفيذ المسارات' + waitingforSuspendingMessage = 'انتظار الإيقاف...' + waitingforResumingMessage = 'انتظار الاستئناف...' } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index 1f99d815d..62ac39bdb 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Angehalten' runningMessage = 'Läuft' openHttpEndpointMessage = 'Öffnen Sie den ersten HTTP-Endpunkt im Standardbrowser.' + suspendingRunspaceMessage = 'Suspension der Runspaces' + resumingRunspaceMessage = 'Fortsetzung der Runspaces' + waitingforSuspendingMessage = 'Warten auf das Suspendieren ...' + waitingforResumingMessage = 'Warten auf das Fortsetzen ...' } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 7c8dff863..b53af8450 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Suspended' runningMessage = 'Running' openHttpEndpointMessage = 'Open the first HTTP endpoint in the default browser.' + suspendingRunspaceMessage = 'Suspending Runspaces' + resumingRunspaceMessage = 'Resuming Runspaces' + waitingforSuspendingMessage = 'Waiting for suspending ...' + waitingforResumingMessage = 'Waiting for resuming ...' } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 2b09c8f7c..b6456f6b3 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Suspended' runningMessage = 'Running' openHttpEndpointMessage = 'Open the first HTTP endpoint in the default browser.' + suspendingRunspaceMessage = 'Suspending Runspaces' + resumingRunspaceMessage = 'Resuming Runspaces' + waitingforSuspendingMessage = 'Waiting for suspending ...' + waitingforResumingMessage = 'Waiting for resuming ...' } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index d10750bba..53a4185e3 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Suspendido' runningMessage = 'En ejecución' openHttpEndpointMessage = 'Abrir el primer endpoint HTTP en el navegador predeterminado.' + suspendingRunspaceMessage = 'Suspendiendo los espacios de ejecución' + resumingRunspaceMessage = 'Reanudando los espacios de ejecución' + waitingforSuspendingMessage = 'Esperando para suspender ...' + waitingforResumingMessage = 'Esperando para reanudar ...' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index d910bdc74..608108d2b 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Suspendu' runningMessage = "En cours d'exécution" openHttpEndpointMessage = 'Ouvrez le premier point de terminaison HTTP dans le navigateur par défaut.' + suspendingRunspaceMessage = 'Suspension des Runspaces' + resumingRunspaceMessage = 'Reprise des Runspaces' + waitingforSuspendingMessage = 'En attente de la suspension ...' + waitingforResumingMessage = 'En attente de la reprise ...' } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 53b29fb34..5e04957b5 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Sospeso' runningMessage = 'In esecuzione' openHttpEndpointMessage = 'Apri il primo endpoint HTTP nel browser predefinito.' + suspendingRunspaceMessage = 'Sospensione degli spazi di esecuzione' + resumingRunspaceMessage = 'Ripresa degli spazi di esecuzione' + waitingforSuspendingMessage = 'Attesa per sospensione ...' + waitingforResumingMessage = 'Attesa per ripresa ...' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 8e6cb3178..f1f642d24 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = '一時停止中' runningMessage = '実行中' openHttpEndpointMessage = 'デフォルトのブラウザで最初の HTTP エンドポイントを開きます。' + suspendingRunspaceMessage = 'ランスペースの停止' + resumingRunspaceMessage = 'ランスペースの再開' + waitingforSuspendingMessage = '停止待機中...' + waitingforResumingMessage = '再開待機中...' } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 6960afe2e..70f8df504 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = '일시 중지됨' runningMessage = '실행 중' openHttpEndpointMessage = '기본 브라우저에서 첫 번째 HTTP 엔드포인트를 엽니다.' + suspendingRunspaceMessage = '런스페이스 일시 정지' + resumingRunspaceMessage = '런스페이스 재개' + waitingforSuspendingMessage = '일시 정지 대기 중...' + waitingforResumingMessage = '재개 대기 중...' } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 2d359cc5f..0e510d4d7 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Gepauzeerd' runningMessage = 'Actief' openHttpEndpointMessage = 'Open het eerste HTTP-eindpunt in de standaardbrowser.' + suspendingRunspaceMessage = 'Wstrzymywanie przestrzeni roboczych' + resumingRunspaceMessage = 'Wznawianie przestrzeni roboczych' + waitingforSuspendingMessage = 'Oczekiwanie na zawieszenie ...' + waitingforResumingMessage = 'Oczekiwanie na wznowienie ...' } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index f003d5036..05cbdaf10 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Wstrzymany' runningMessage = 'Działa' openHttpEndpointMessage = 'Otwórz pierwszy punkt końcowy HTTP w domyślnej przeglądarce.' + suspendingRunspaceMessage = 'Wstrzymywanie przestrzeni roboczych' + resumingRunspaceMessage = 'Wznawianie przestrzeni roboczych' + waitingforSuspendingMessage = 'Oczekiwanie na zawieszenie ...' + waitingforResumingMessage = 'Oczekiwanie na wznowienie ...' } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index de9a4340c..074152254 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = 'Suspenso' runningMessage = 'Executando' openHttpEndpointMessage = 'Abrir o primeiro endpoint HTTP no navegador padrão.' + suspendingRunspaceMessage = 'Suspensão dos Runspaces' + resumingRunspaceMessage = 'Retomada dos Runspaces' + waitingforSuspendingMessage = 'Esperando para suspender ...' + waitingforResumingMessage = 'Esperando para retomar ...' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 46b3cf791..df11e8119 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -305,4 +305,8 @@ suspendedMessage = '已暂停' runningMessage = '运行中' openHttpEndpointMessage = '在默认浏览器中打开第一个 HTTP 端点。' + suspendingRunspaceMessage = '暂停运行空间' + resumingRunspaceMessage = '恢复运行空间' + waitingforSuspendingMessage = '等待暂停...' + waitingforResumingMessage = '等待恢复...' } \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index e68ec7ff3..0e928d484 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -506,24 +506,36 @@ function Suspend-PodeServerInternal { $Timeout = 30 ) try { + + # Inform user that the server is suspending + $suspendActivityId = (Get-Random) # Inform user that the server is suspending - Write-PodeHost $PodeLocale.SuspendingMessage -ForegroundColor Yellow + Write-Progress -Activity $PodeLocale.SuspendingMessage ` + -Status 'Invoke Suspend Event' ` + -PercentComplete 3 ` + -Id $suspendActivityId # Trigger the Suspend event Invoke-PodeEvent -Type Suspend # Update the server's suspended state $PodeContext.Server.Suspended = $true - start-sleep 2 + start-sleep 1 try { + Write-Progress -Activity $PodeLocale.SuspendingMessage ` + -Status $PodeLocale.suspendingRunspaceMessage ` + -PercentComplete 5 ` + -Id $suspendActivityId + # Retrieve all runspaces related to Pode ordered by name so the Main runspace are the first to be suspended (To avoid the process hunging) $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Tasks_*' -or $_.Name -like 'Pode_Schedules_*' } | Sort-Object Name + # Initialize the master progress bar $runspaceCount = $runspaces.Count $currentRunspaceIndex = 0 $masterActivityId = (Get-Random) # Unique ID for master progress - $runspaces | Foreach-Object { + $runspaces | Foreach-Object { $originalName = $_.Name # Initialize progress bar variables $startTime = [DateTime]::UtcNow @@ -533,46 +545,49 @@ function Suspend-PodeServerInternal { $currentRunspaceIndex++ $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) - Write-Progress -Activity 'Suspending Runspaces' ` - -Status "Processing runspace $($currentRunspaceIndex) of $($runspaceCount): $($_.Name)" ` + Write-Progress -Activity $PodeLocale.suspendingRunspaceMessage ` + -Status "[$($currentRunspaceIndex)/$($runspaceCount)]: $($_.Name)" ` -PercentComplete $masterPercentComplete ` - -Id $masterActivityId + -Id $masterActivityId ` + -ParentId $suspendActivityId + + Write-Progress -Activity $PodeLocale.SuspendingMessage ` + -Status $PodeLocale.suspendingRunspaceMessage -PercentComplete ($masterPercentComplete - 5) ` + -Id $suspendActivityId + # Suspend the runspace Enable-RunspaceDebug -BreakAll -Runspace $_ try { # Initial progress bar display - Write-Progress -Activity "Suspending Runspace $($_.Name)" ` - -Status 'Waiting for suspension...' ` - -PercentComplete 0 ` - -Id $childActivityId ` - -ParentId $masterActivityId + Write-Progress -Activity "$($PodeLocale.suspendingRunspaceMessage) $($_.Name)" ` + -Status $PodeLocale.waitingforSuspendingMessage ` + -PercentComplete 0 -Id $childActivityId -ParentId $masterActivityId while (! $_.debugger.InBreakpoint) { # Update elapsed time and progress $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds $percentComplete = [math]::Min(($elapsedTime / $Timeout) * 100, 100) - Write-Progress -Activity "Suspending Runspace $($_.Name)" ` - -Status 'Waiting for suspension...' ` + Write-Progress -Activity "$($PodeLocale.suspendingRunspaceMessage) $($_.Name)" ` + -Status $PodeLocale.suspendingRunspaceMessage ` -PercentComplete $percentComplete ` -Id $childActivityId ` -ParentId $masterActivityId if ($_.Name.StartsWith('_')) { - Write-PodeHost "$originalName runspace has beed completed" Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + Write-Verbose "$originalName runspace has beed completed" break } # Check for timeout if ($elapsedTime -ge $Timeout) { Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId - Write-PodeHost "$($_.Name) failed (Timeout reached after $Timeout seconds.)" + Write-Verbose "$($_.Name) failed (Timeout reached after $Timeout seconds.)" break } Start-Sleep -Milliseconds 1000 } - } catch { $_ | Write-PodeErrorLog @@ -580,6 +595,9 @@ function Suspend-PodeServerInternal { finally { # Completion message Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + Write-Progress -Activity $PodeLocale.SuspendingMessage ` + -Status $PodeLocale.suspendingRunspaceMessage -PercentComplete 100 -Id $suspendActivityId + Start-Sleep -Milliseconds 1000 } } @@ -589,7 +607,7 @@ function Suspend-PodeServerInternal { } finally { # Clear master progress bar once all runspaces are processed - Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $masterActivityId + Write-Progress -Activity $PodeLocale.suspendingRunspaceMessage -Completed -Id $masterActivityId -ParentId $suspendActivityId } # Short pause before refreshing the console @@ -602,7 +620,10 @@ function Suspend-PodeServerInternal { # Log any errors that occur $_ | Write-PodeErrorLog } - + finally { + # Clear master progress bar once all suspension is completed + Write-Progress -Activity $PodeLocale.SuspendingMessage -Completed -Id $suspendActivityId + } } @@ -624,10 +645,138 @@ function Suspend-PodeServerInternal { #> function Resume-PodeServerInternal { + + param( + [int] + $Timeout = 30 + ) try { - # Inform user that the server is resuming - Write-PodeHost $PodeLocale.ResumingMessage -NoNewline -ForegroundColor Yellow + # Inform user that the server is suspending + $ResumingActivityId = (Get-Random) + # Inform user that the server is suspending + + Write-Progress -Activity $PodeLocale.ResumingMessage ` + -Status 'Invoke Resume Event' ` + -PercentComplete 3 ` + -Id $ResumingActivityId + # Trigger the Resume event + Invoke-PodeEvent -Type Resume + + # Update the server's suspended state + $PodeContext.Server.Suspended = $false + start-sleep 1 + try { + Write-Progress -Activity $PodeLocale.ResumingMessage ` + -Status $PodeLocale.resumingRunspaceMessage ` + -PercentComplete 5 ` + -Id $ResumingActivityId + + # Disable debugging for each runspace to restore normal execution + $runspaces = Get-Runspace | Where-Object { $_.Debugger.InBreakpoint } + + # Initialize the master progress bar + $runspaceCount = $runspaces.Count + $currentRunspaceIndex = 0 + $masterActivityId = (Get-Random) # Unique ID for master progress + + $runspaces | Foreach-Object { + $originalName = $_.Name + # Initialize progress bar variables + $startTime = [DateTime]::UtcNow + $elapsedTime = 0 + $childActivityId = (Get-Random) # Unique ID for child progress bar + # Update master progress bar + $currentRunspaceIndex++ + $masterPercentComplete = [math]::Round(($currentRunspaceIndex / $runspaceCount) * 100) + + Write-Progress -Activity $PodeLocale.resumingRunspaceMessage ` + -Status "[$($currentRunspaceIndex)/$($runspaceCount)]: $($_.Name)" ` + -PercentComplete $masterPercentComplete ` + -Id $masterActivityId ` + -ParentId $ResumingActivityId + + Write-Progress -Activity $PodeLocale.ResumingMessage ` + -Status $PodeLocale.resumingRunspaceMessage -PercentComplete ($masterPercentComplete - 5) ` + -Id $ResumingActivityId + + # Resume the runspace + Disable-RunspaceDebug -Runspace $_ + try { + # Initial progress bar display + Write-Progress -Activity "$($PodeLocale.resumingRunspaceMessage) $($_.Name)" ` + -Status $PodeLocale.waitingforResumingMessage ` + -PercentComplete 0 -Id $childActivityId -ParentId $masterActivityId + + while ( $_.debugger.InBreakpoint) { + # Update elapsed time and progress + $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds + $percentComplete = [math]::Min(($elapsedTime / $Timeout) * 100, 100) + + Write-Progress -Activity "$($PodeLocale.resumingRunspaceMessage) $($_.Name)" ` + -Status ($PodeLocale.resumingRunspaceMessage) ` + -PercentComplete $percentComplete ` + -Id $childActivityId ` + -ParentId $masterActivityId + + # Check for timeout + if ($elapsedTime -ge $Timeout) { + Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + Write-Verbose "$($_.Name) failed (Timeout reached after $Timeout seconds.)" + break + } + + Start-Sleep -Milliseconds 1000 + } + + } + catch { + $_ | Write-PodeErrorLog + } + finally { + # Completion message + Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + Write-Progress -Activity $PodeLocale.ResumingMessage ` + -Status $PodeLocale.resumingRunspaceMessage -PercentComplete 100 -Id $ResumingActivityId + + Start-Sleep -Milliseconds 1000 + } + } + } + catch { + $_ | Write-PodeErrorLog + } + finally { + # Clear master progress bar once all runspaces are processed + Write-Progress -Activity $PodeLocale.resumingRunspaceMessage -Completed -Id $masterActivityId -ParentId $ResumingActivityId + } + + # Short pause before refreshing the console + Start-Sleep -Seconds 1 + + # Clear the host and display header information + Show-PodeConsoleInfo -ShowHeader + } + catch { + # Log any errors that occur + $_ | Write-PodeErrorLog + } + finally { + # Clear master progress bar once all suspension is completed + Write-Progress -Activity $PodeLocale.ResumingMessage -Completed -Id $ResumingActivityId + + # Reinitialize the CancellationTokenSource for future suspension/resumption + Reset-PodeCancellationToken -Type Resume + } + <# +try{ + + # Inform user that the server is resuming + # Write-PodeHost $PodeLocale.ResumingMessage -NoNewline -ForegroundColor Yellow + Write-Progress -Activity $PodeLocale.ResumingMessage ` + -Status 'Invoke Resume Event' ` + -PercentComplete 5 ` + -Id $suspendActivityId # Trigger the Resume event Invoke-PodeEvent -Type Resume @@ -638,7 +787,12 @@ function Resume-PodeServerInternal { Start-Sleep -Seconds 1 # Disable debugging for each runspace to restore normal execution - Get-Runspace | Where-Object { $_.Debugger.InBreakpoint } | Disable-RunspaceDebug + $runspaces= Get-Runspace | Where-Object { $_.Debugger.InBreakpoint } + + + + + Disable-RunspaceDebug # Inform user that the resume process is complete Write-PodeHost 'Done' -ForegroundColor Green @@ -657,5 +811,5 @@ function Resume-PodeServerInternal { # Reinitialize the CancellationTokenSource for future suspension/resumption Reset-PodeCancellationToken -Type Resume - } + }#> } \ No newline at end of file From a9dfecf7aab86ae391a1674ddf1598d8d54a398c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 8 Dec 2024 09:30:50 -0800 Subject: [PATCH 34/34] Changed the runspace enumeration logic --- src/Embedded/DebuggerHandler.cs | 122 ++++++++++++++++++++++++++------ src/Private/Context.ps1 | 55 ++++++++------ src/Private/Dump.ps1 | 53 ++++++-------- src/Private/FileWatchers.ps1 | 2 +- src/Private/PodeServer.ps1 | 6 +- src/Private/Runspaces.ps1 | 15 ++-- src/Private/Server.ps1 | 1 - src/Private/SmtpServer.ps1 | 4 +- src/Private/TcpServer.ps1 | 2 +- src/Private/WebSockets.ps1 | 2 +- 10 files changed, 175 insertions(+), 87 deletions(-) diff --git a/src/Embedded/DebuggerHandler.cs b/src/Embedded/DebuggerHandler.cs index c0ca7023e..73c6d96b4 100644 --- a/src/Embedded/DebuggerHandler.cs +++ b/src/Embedded/DebuggerHandler.cs @@ -16,13 +16,12 @@ public class DebuggerHandler : IDisposable // Flag to indicate whether the DebuggerStop event has been triggered public bool IsEventTriggered { get; private set; } - // Flag to control whether variables should be collected during the DebuggerStop event - private bool _collectVariables = true; + private readonly bool _verboseEnabled; // Indicates if verbose is enabled // Runspace object to store the runspace that the debugger is attached to private readonly Runspace _runspace; - public DebuggerHandler(Runspace runspace, bool collectVariables = true) + public DebuggerHandler(Runspace runspace, bool verboseEnabled = false) { // Ensure the runspace is not null if (runspace == null) @@ -30,8 +29,9 @@ public DebuggerHandler(Runspace runspace, bool collectVariables = true) throw new ArgumentNullException("runspace"); // Use string literal for older C# compatibility } + _verboseEnabled = verboseEnabled; + _runspace = runspace; - _collectVariables = collectVariables; // Initialize the event handler _debuggerStopHandler = OnDebuggerStop; @@ -40,13 +40,84 @@ public DebuggerHandler(Runspace runspace, bool collectVariables = true) // Initialize variables collection Variables = new PSDataCollection(); IsEventTriggered = false; + + WriteVerbose("DebuggerHandler initialized. Enabling debug break on all execution contexts."); + + // Enable debugging and break on all contexts + // EnableDebugBreakAll(); } + /// + /// Enables debugging for the entire runspace and breaks on all execution contexts. + /// + private void EnableDebugBreakAll() + { + if (_runspace.Debugger.InBreakpoint) + { + throw new InvalidOperationException("The debugger is already active and in a breakpoint state."); + } + + WriteVerbose("Enabling debug break on all contexts..."); + + // Console.WriteLine("Enabling debugging with BreakAll mode..."); + _runspace.Debugger.SetDebugMode(DebugModes.LocalScript | DebugModes.RemoteScript); + } + + + /// + /// Exits the debugger by processing the 'exit' command. + /// + private void ExitDebugger(int timeoutInSeconds = 10) + { + // Check if exiting the debugger is required + if (_runspace.Debugger.InBreakpoint) + { + WriteVerbose("Exiting the debugger..."); + + // Create a command to execute the "exit" command + var command = new PSCommand(); + command.AddCommand("exit"); + + // Execute the command within the debugger + var outputCollection = new PSDataCollection(); + _runspace.Debugger.ProcessCommand(command, outputCollection); + } + else + { + throw new InvalidOperationException("The debugger is not in a breakpoint state."); + } + + // Start a stopwatch to enforce the timeout + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + // Wait until debugger exits or timeout is reached + while (_runspace.Debugger.InBreakpoint) + { + if (stopwatch.Elapsed.TotalSeconds >= timeoutInSeconds) + { + WriteVerbose("Timeout reached while waiting for the debugger to exit."); + + break; + } + + System.Threading.Thread.Sleep(1000); // Wait for 1 second + } + + stopwatch.Stop(); + WriteVerbose("ExitDebugger method completed."); + } + + + /// /// Detaches the DebuggerStop event handler and releases resources. /// public void Dispose() { + + ExitDebugger(); + + if (_debuggerStopHandler != null) { _runspace.Debugger.DebuggerStop -= _debuggerStopHandler; @@ -58,37 +129,48 @@ public void Dispose() GC.SuppressFinalize(this); } + /// /// Event handler for the DebuggerStop event. /// private void OnDebuggerStop(object sender, DebuggerStopEventArgs args) { IsEventTriggered = true; - + WriteVerbose("DebuggerStop event triggered."); // Cast the sender to a Debugger object var debugger = sender as Debugger; if (debugger == null) { return; } - if (_collectVariables) + + // Enable step mode for command execution + debugger.SetDebuggerStepMode(true); + + // Create the command to execute + var command = new PSCommand(); + command.AddCommand("Get-PodeDumpScopedVariable"); + + // Execute the command within the debugger + var outputCollection = new PSDataCollection(); + debugger.ProcessCommand(command, outputCollection); + + // Collect the variables if required + foreach (var output in outputCollection) { - // Enable step mode for command execution - debugger.SetDebuggerStepMode(true); + Variables.Add(output); + } + } - // Create the command to execute - var command = new PSCommand(); - command.AddCommand("Get-PodeDumpScopedVariable"); - // Execute the command within the debugger - var outputCollection = new PSDataCollection(); - debugger.ProcessCommand(command, outputCollection); - // Collect the variables if required - foreach (var output in outputCollection) - { - Variables.Add(output); - } + private void WriteVerbose(string message) + { + // Check if verbose output is enabled + if (_verboseEnabled) + { + Console.WriteLine($"VERBOSE: {message}"); } } + } -} +} \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index b19f00c11..7570a9f11 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -554,47 +554,53 @@ function New-PodeRunspacePool { # main runspace - for timers, schedules, etc $totalThreadCount = ($threadsCounts.Values | Measure-Object -Sum).Sum $PodeContext.RunspacePools.Main = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, $totalThreadCount, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } # web runspace - if we have any http/s endpoints if (Test-PodeEndpointByProtocolType -Type Http) { $PodeContext.RunspacePools.Web = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # smtp runspace - if we have any smtp endpoints if (Test-PodeEndpointByProtocolType -Type Smtp) { $PodeContext.RunspacePools.Smtp = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # tcp runspace - if we have any tcp endpoints if (Test-PodeEndpointByProtocolType -Type Tcp) { $PodeContext.RunspacePools.Tcp = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 1), $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # signals runspace - if we have any ws/s endpoints if (Test-PodeEndpointByProtocolType -Type Ws) { $PodeContext.RunspacePools.Signals = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 2), $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, ($PodeContext.Threads.General + 2), $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # web socket connections runspace - for receiving data for external sockets if (Test-PodeWebSocketsExist) { $PodeContext.RunspacePools.WebSockets = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.WebSockets + 1, $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.WebSockets + 1, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } New-PodeWebSocketReceiver @@ -603,40 +609,45 @@ function New-PodeRunspacePool { # setup timer runspace pool -if we have any timers if (Test-PodeTimersExist) { $PodeContext.RunspacePools.Timers = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Timers, $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Timers, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # setup schedule runspace pool -if we have any schedules if (Test-PodeSchedulesExist) { $PodeContext.RunspacePools.Schedules = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Schedules, $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Schedules, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # setup tasks runspace pool -if we have any tasks if (Test-PodeTasksExist) { $PodeContext.RunspacePools.Tasks = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Tasks, $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Tasks, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # setup files runspace pool -if we have any file watchers if (Test-PodeFileWatchersExist) { $PodeContext.RunspacePools.Files = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Files + 1, $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, $PodeContext.Threads.Files + 1, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } } # setup gui runspace pool (only for non-ps-core) - if gui enabled if (Test-PodeGuiEnabled) { $PodeContext.RunspacePools.Gui = @{ - Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host) - State = 'Waiting' + Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 } $PodeContext.RunspacePools.Gui.Pool.ApartmentState = 'STA' diff --git a/src/Private/Dump.ps1 b/src/Private/Dump.ps1 index eeeb21fc9..c84ed9c3e 100644 --- a/src/Private/Dump.ps1 +++ b/src/Private/Dump.ps1 @@ -157,7 +157,8 @@ function Invoke-PodeDumpInternal { $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_*' -and ` $_.Name -notlike '*__pode_session_inmem_cleanup__*' -and ` # $_.Name -notlike 'Pode_*_Listener_*' -and ` - $_.Name -notlike 'Pode_*_KeepAlive_*' #-and ` + $_.Name -notlike 'Pode_*_KeepAlive_*' -and ` + $_.Name -notlike '_Pode_*' # $_.Name -notlike 'Pode_Signals_Broadcaster_*' } | Sort-Object Name @@ -167,7 +168,6 @@ function Invoke-PodeDumpInternal { # Initialize the master progress bar $runspaceCount = $runspaces.Count + 1 $currentRunspaceIndex = 0 - $skipped = 0 $mainActivityId = (Get-Random) # Unique ID for master progress try { foreach ($r in $runspaces) { @@ -186,7 +186,7 @@ function Invoke-PodeDumpInternal { Id = $r.Id Name = @{ $r.Name = @{ - ScopedVariables = Suspend-PodeRunspace -Runspace $r -ParentActivityId $mainActivityId -CollectVariable + ScopedVariables = Suspend-PodeRunspace -Runspace $r -ParentActivityId $mainActivityId } } InitialSessionState = $r.InitialSessionState @@ -202,7 +202,6 @@ function Invoke-PodeDumpInternal { Start-PodeSleep -Seconds 3 -ParentId $mainActivityId -ShowProgress # Clear master progress bar once all runspaces are processed Write-Progress -Activity 'Suspending Runspaces' -Completed -Id $mainActivityId - Write-Verbose "$skipped of $runspaceCount runspaces are not in a busy state" } # $outputCollection = [System.Management.Automation.PSDataCollection[psobject]]::new() # $cmd=[System.Management.Automation.PSCommand]::new().AddCommand('Continue') @@ -317,17 +316,14 @@ function Suspend-PodeRunspace { [Parameter()] [int] - $ParentActivityId, - - [switch] - $CollectVariable + $ParentActivityId ) try { # Initialize debugger - $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace) + $debugger = [Pode.Embedded.DebuggerHandler]::new($Runspace, ($VerbosePreference -eq 'Continue')) Enable-RunspaceDebug -BreakAll -Runspace $Runspace - + #$debugger.EnableDebugBreakAll() # Initialize progress bar variables $startTime = [DateTime]::UtcNow $elapsedTime = 0 @@ -343,7 +339,7 @@ function Suspend-PodeRunspace { while (!$debugger.IsEventTriggered) { # Update elapsed time and progress $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds - $percentComplete = [math]::Min(($elapsedTime / $Timeout) * 100, 100) + $percentComplete = [math]::Min(($elapsedTime / $Timeout) * 100, 90) Write-Progress -Activity "Suspending Runspace $($Runspace.Name)" ` -Status 'Waiting for suspension...' ` @@ -351,6 +347,11 @@ function Suspend-PodeRunspace { -Id $childActivityId ` -ParentId $ParentActivityId + if ($Runspace.Name.StartsWith('_')) { + Write-Progress -Completed -Id $childActivityId -ParentId $masterActivityId + Write-Verbose "$originalName runspace has beed completed" + return @{} + } # Check for timeout if ($elapsedTime -ge $Timeout) { Write-Progress -Completed -Id $childActivityId -ParentId $ParentActivityId @@ -360,9 +361,8 @@ function Suspend-PodeRunspace { Start-Sleep -Milliseconds 1000 } - if ($CollectVariable) { - $r = $debugger.Variables - <# $variables = [System.Management.Automation.PSDataCollection[psobject]]::new(); + $r = $debugger.Variables + <# $variables = [System.Management.Automation.PSDataCollection[psobject]]::new(); $outputCollection = [System.Management.Automation.PSDataCollection[psobject]]::new() $cmd = [System.Management.Automation.PSCommand]::new().AddCommand('Get-PodeDumpScopedVariable') $Runspace.Debugger.ProcessCommand($cmd, $outputCollection) @@ -371,39 +371,32 @@ function Suspend-PodeRunspace { { $variables.Add($output) }#> - # Return the collected variables - return $r - } - else { - return $true - } + # Return the collected variables + return $r + } catch { # Log the error details using Write-PodeErrorLog for troubleshooting. $_ | Write-PodeErrorLog } finally { - Start-Sleep 1 + # Start-Sleep 1 # Clean up debugger resources and disable debugging if ($null -ne $debugger) { $debugger.Dispose() } - if ($CollectVariable) { - Disable-RunspaceDebug -Runspace $Runspace - } + # if ($CollectVariable) { + # Disable-RunspaceDebug -Runspace $Runspace + # } # Completion message + # Start-Sleep 1 Write-Progress -Completed -Id $childActivityId } # Fallback returns for unhandled scenarios - if ($CollectVariable) { - return @{ } - } - else { - return $false - } + return @{} } diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index 2b0fb9849..4132a91ad 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -137,7 +137,7 @@ function Start-PodeFileWatcherRunspace { } 1..$PodeContext.Threads.Files | ForEach-Object { - Add-PodeRunspace -Type Files -Name 'Watcher' -Id $_ -ScriptBlock $watchScript -Parameters @{ 'Watcher' = $watcher ; 'ThreadId' = $_ } + Add-PodeRunspace -Type Files -Name 'Watcher' -ScriptBlock $watchScript -Parameters @{ 'Watcher' = $watcher ; 'ThreadId' = $_ } } # script to keep file watcher server alive until cancelled diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 43be04485..57bad5c44 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -297,7 +297,7 @@ function Start-PodeWebServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Web -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Web -Name 'Listener' -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } } @@ -456,7 +456,7 @@ function Start-PodeWebServer { $_.Exception | Write-PodeErrorLog -CheckInnerException throw $_.Exception } - + # end do-while } while (Test-PodeSuspensionToken) # Check for suspension or dump tokens and wait for the debugger to reset if active @@ -464,7 +464,7 @@ function Start-PodeWebServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Signals -Name 'Broadcaster' -Id $_ -ScriptBlock $clientScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Signals -Name 'Broadcaster' -ScriptBlock $clientScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } } diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index a58025664..e05488ad8 100644 --- a/src/Private/Runspaces.ps1 +++ b/src/Private/Runspaces.ps1 @@ -31,6 +31,9 @@ .PARAMETER PassThru If specified, returns the pipeline and handler for custom processing. +.PARAMETER Name + If specified, is used as base name for the runspace. + .EXAMPLE Add-PodeRunspace -Type 'Tasks' -ScriptBlock { # Your script code here @@ -66,10 +69,7 @@ function Add-PodeRunspace { $PassThru, [string] - $Name, - - [string] - $Id = '1' + $Name = 'generic' ) try { @@ -108,12 +108,15 @@ function Add-PodeRunspace { $ps = [powershell]::Create() $ps.RunspacePool = $PodeContext.RunspacePools[$Type].Pool + # $Id = (++$PodeContext.RunspacePools[$Type].LastId) + + # Add the script block and parameters to the pipeline. $null = $ps.AddScript($openRunspaceScript) $null = $ps.AddParameters( @{ 'Type' = $Type - 'Name' = "Pode_$($Type)_$($Name)_$($Id)" + 'Name' = "Pode_$($Type)_$($Name)_$((++$PodeContext.RunspacePools[$Type].LastId))" 'NoProfile' = $NoProfile.IsPresent } ) @@ -304,7 +307,7 @@ function Close-PodeRunspace { } } - <# +<# .SYNOPSIS Resets the name of the current Pode runspace by modifying its structure. diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 0e928d484..3ad944ef1 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -681,7 +681,6 @@ function Resume-PodeServerInternal { $masterActivityId = (Get-Random) # Unique ID for master progress $runspaces | Foreach-Object { - $originalName = $_.Name # Initialize progress bar variables $startTime = [DateTime]::UtcNow $elapsedTime = 0 diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index dd025c0ec..cf273df37 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -88,7 +88,7 @@ function Start-PodeSmtpServer { [int] $ThreadId ) - + do { try { while ($Listener.IsConnected -and !$PodeContext.Tokens.Terminate.IsCancellationRequested) { @@ -182,7 +182,7 @@ function Start-PodeSmtpServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Smtp -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Smtp -Name 'Listener' -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } # script to keep smtp server listening until cancelled diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 416b9f26c..81db259c1 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -200,7 +200,7 @@ function Start-PodeTcpServer { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.General | ForEach-Object { - Add-PodeRunspace -Type Tcp -Name 'Listener' -Id $_ -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } + Add-PodeRunspace -Type Tcp -Name 'Listener' -ScriptBlock $listenScript -Parameters @{ 'Listener' = $listener; 'ThreadId' = $_ } } # script to keep tcp server listening until cancelled diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index a4724c1aa..4605758a6 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -112,7 +112,7 @@ function Start-PodeWebSocketRunspace { # start the runspace for listening on x-number of threads 1..$PodeContext.Threads.WebSockets | ForEach-Object { - Add-PodeRunspace -Type WebSockets -Name 'Receiver' -Id $_ -ScriptBlock $receiveScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver; 'ThreadId' = $_ } + Add-PodeRunspace -Type WebSockets -Name 'Receiver' -ScriptBlock $receiveScript -Parameters @{ 'Receiver' = $PodeContext.Server.WebSockets.Receiver; 'ThreadId' = $_ } } # script to keep websocket server receiving until cancelled