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/Console.md b/docs/Getting-Started/Console.md new file mode 100644 index 000000000..4d1ab0564 --- /dev/null +++ b/docs/Getting-Started/Console.md @@ -0,0 +1,318 @@ +# Console Output + +Pode introduces a configurable **console output** feature, enabling you to monitor server activities, control its behavior, and customize the console's appearance and functionality to suit your needs. The console displays key server information such as active endpoints, OpenAPI documentation links, and control commands. + +Additionally, several console settings can be configured dynamically when starting the Pode server using the `Start-PodeServer` function. + +--- + +## Typical Console Output + +Below is an example of the console output during server runtime: + +```plaintext +[2025-01-12 10:28:05] Pode [dev] (PID: 29748) [Running] +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Listening on 10 endpoint(s) [4 thread(s)]: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + - HTTP : http://localhost:8081/ [Name:General, Default] + http://localhost:8083/ [DualMode] + http://localhost:8091/ [Name:WS] + - HTTPS : https://localhost:8082/ + - SMTP : smtp://localhost:8025 + - SMTPS : smtps://localhost:8026 + - TCP : tcp://localhost:8100 + - TCPS : tcps://localhost:9002 + - WS : ws://localhost:8091 [Name:WS1] + - WSS : wss://localhost:8093 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +OpenAPI Information: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 'default': + Specification: + - http://localhost:8081/docs/openapi + - http://localhost:8083/docs/openapi + - http://localhost:8091/docs/openapi + - https://localhost:8082/docs/openapi + Documentation: + - http://localhost:8081/docs + - http://localhost:8083/docs + - http://localhost:8091/docs + - https://localhost:8082/docs +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Server Control Commands: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Ctrl-C : Gracefully terminate the server. +Ctrl-R : Restart the server and reload configurations. +Ctrl-P : Suspend the server. +Ctrl-D : Disable Server +Ctrl-H : Hide Help +Ctrl-B : Open the first HTTP endpoint in the default browser. + ---- +Ctrl-M : Show Metrics +Ctrl-E : Hide Endpoints +Ctrl-O : Hide OpenAPI +Ctrl-L : Clear the Console +Ctrl-Q : Enable Quiet Mode +``` + +--- + +## Console Configuration + +The behavior, appearance, and functionality of the console are highly customizable through the `server.psd1` configuration file. Additionally, some console-related settings can be configured dynamically via parameters in the `Start-PodeServer` function. + +### Configurable Settings via `Start-PodeServer` + +| **Parameter** | **Description** | +|-----------------------|--------------------------------------------------------------------------------------------------| +| `DisableTermination` | Prevents termination, suspension, or resumption of the server via keyboard interactive commands. | +| `DisableConsoleInput` | Disables all console keyboard interactions for the server. | +| `ClearHost` | Clears the console whenever the server changes state (e.g., running → suspend → resume). | +| `Quiet` | Suppresses all console output for a clean execution experience. | +| `HideOpenAPI` | Hides OpenAPI details such as specification and documentation URLs in the console output. | +| `HideEndpoints` | Hides the list of active endpoints in the console output. | +| `ShowHelp` | Displays a help menu in the console with available control commands. | +| `Daemon` | Configures the server to run as a daemon with minimal console interaction and output. | + +#### Example Usage + +```powershell +# Start a server with custom console settings +Start-PodeServer -HideEndpoints -HideOpenAPI -ClearHost { + # Server logic +} +``` + +```powershell +# Start a server in quiet mode without console interaction +Start-PodeServer -DisableConsoleInput -Quiet { + # Server logic +} +``` + +### Default Configuration + +Here is the default `Server.Console` configuration: + +```powershell +@{ + Server = @{ + Console = @{ + DisableTermination = $false # Prevent Ctrl+C from terminating the server. + DisableConsoleInput = $false # Disable all console input controls. + Quiet = $false # Suppress console output. + ClearHost = $false # Clear the console output at startup. + ShowOpenAPI = $true # Display OpenAPI information. + ShowEndpoints = $true # Display listening endpoints. + ShowHelp = $false # Show help instructions in the console. + ShowDivider = $true # Display dividers between sections. + DividerLength = 75 # Length of dividers in the console. + ShowTimeStamp = $true # Display timestamp in the header. + + Colors = @{ # Customize console colors. + Header = 'White' # The server's header section, including the Pode version and timestamp. + EndpointsHeader = 'Yellow' # The header for the endpoints list. + Endpoints = 'Cyan' # The endpoints themselves, including protocol and URLs. + OpenApiUrls = 'Cyan' # URLs listed under the OpenAPI information section. + OpenApiHeaders = 'Yellow' # Section headers for OpenAPI information. + OpenApiTitles = 'White' # The OpenAPI "default" title. + OpenApiSubtitles = 'Yellow' # Subtitles under OpenAPI (e.g., Specification, Documentation). + HelpHeader = 'Yellow' # Header for the Help section. + HelpKey = 'Green' # Key bindings listed in the Help section (e.g., Ctrl+c). + HelpDescription = 'White' # Descriptions for each Help section key binding. + HelpDivider = 'Gray' # Dividers used in the Help section. + Divider = 'DarkGray' # Dividers between console sections. + MetricsHeader = 'Yellow' # Header for the Metric section. + MetricsLabel = 'White' # Labels for values displayed in the Metrics section. + MetricsValue = 'Green' # The actual values displayed in the Metrics section. + } + + KeyBindings = @{ # Define custom key bindings for controls. + Browser = 'B' # Open the default browser. + Help = 'H' # Show/hide help instructions. + OpenAPI = 'O' # Show/hide OpenAPI information. + Endpoints = 'E' # Show/hide endpoints. + Clear = 'L' # Clear the console output. + Quiet = 'Q' # Toggle quiet mode. + Terminate = 'C' # Terminate the server. + Restart = 'R' # Restart the server. + Disable = 'D' # Disable the server. + Suspend = 'P' # Suspend the server. + Metrics = 'M' # Show Metrics. + } + } + } +} +``` + +> **Tip:** The `KeyBindings` property uses the `[System.ConsoleKey]` type. For a complete list of valid values, refer to the [ConsoleKey Enum documentation](https://learn.microsoft.com/en-us/dotnet/api/system.consolekey?view=net-9.0). This resource provides all possible keys that can be used with `KeyBindings`. + +## Examples + +### **Enable Quiet Mode** + +Suppress all console output by enabling quiet mode via `Start-PodeServer`: + +```powershell +Start-PodeServer -Quiet { + # Server logic +} +``` + +### **Custom Divider Style** + +Change the divider length and disable dividers via the `server.psd1` file: + +```powershell +@{ + Server = @{ + Console = @{ + ShowDivider = $false + DividerLength = 100 + KeyBindings = @{ + Browser = 'D9' # Open the default browser with the nmumber 9. + Metrics = 'NumPad5' # Show Metrics with the 5 key on the numeric keypad. + Restart = 'F7' # Restart the server with the F7 key. + } + } + } +} +``` + +### **Custom Key Bindings** + +Redefine the key for terminating the server: + +```powershell +@{ + Server = @{ + Console = @{ + KeyBindings = @{ + Terminate = 'x' + } + } + } +} +``` + +--- + +## Customizing Console Colors + +The console colors are fully customizable via the `Colors` section of the configuration. Each element of the console can have its color defined using PowerShell color names. Here’s what each color setting controls: + +### Color Settings + +| **Key** | **Default Value** | **Description** | +|---------------------|-------------------|------------------------------------------------------------------------| +| `Header` | `White` | The server's header section, including the Pode version and timestamp. | +| `EndpointsHeader` | `Yellow` | The header for the endpoints list. | +| `Endpoints` | `Cyan` | The endpoints themselves, including protocol and URLs. | +| `EndpointsProtocol` | `White` | The endpoints protocol. | +| `EndpointsFlag` | `Gray` | The endpoints flags. | +| `EndpointsName` | `Magenta` | The endpoints name. | +| `OpenApiUrls` | `Cyan` | URLs listed under the OpenAPI information section. | +| `OpenApiHeaders` | `Yellow` | Section headers for OpenAPI information. | +| `OpenApiTitles` | `White` | The OpenAPI "default" title. | +| `OpenApiSubtitles` | `Yellow` | Subtitles under OpenAPI (e.g., Specification, Documentation). | +| `HelpHeader` | `Yellow` | Header for the Help section. | +| `HelpKey` | `Green` | Key bindings listed in the Help section (e.g., `Ctrl+c`). | +| `HelpDescription` | `White` | Descriptions for each Help section key binding. | +| `HelpDivider` | `Gray` | Dividers used in the Help section. | +| `Divider` | `DarkGray` | Dividers between console sections. | +| `MetricsHeader` | `Yellow` | Header for the Metrics section. | +| `MetricsLabel` | `White` | Labels for values displayed in the Metrics section. | +| `MetricsValue` | `Green` | The actual values displayed in the Metrics section. | + +> **Tip:** Test your chosen colors against your terminal's background to ensure readability. + +--- + +## Key Features + +### **1. Customizable Appearance** + +- **Colors**: Define colors for headers, endpoints, dividers, and other elements. +- **Dividers**: Enable or disable dividers and adjust their length for better visual separation. + +### **2. Configurable Behavior** + +- **Disable Termination**: Prevent `Ctrl+C` from stopping the server. +- **Quiet Mode**: Suppress all console output for a cleaner view. +- **Timestamp Display**: Enable or disable timestamps in the console header. + +### **3. Interactive Controls** + +- Control server behavior using keyboard shortcuts. For example: + - **Ctrl+C**: Gracefully terminate the server. + - **Ctrl+R**: Restart the server. + - **Ctrl+D**: Disable the server, preventing new requests. + - **Ctrl+P**: Suspend the server temporarily. + - **Ctrl+H**: Display or hide help instructions. + - **Ctrl+M**: Display the server metrics. + +### **4. OpenAPI Integration** + +- Automatically display links to OpenAPI specifications and documentation for the active endpoints. + +### **5. Enhanced Visibility** + +- Use colors to highlight key sections, such as endpoints or OpenAPI URLs, improving readability. + +## Examples + +### **Change Header and Endpoint Colors** + +```powershell +@{ + Colors = @{ + Header = 'Green' + Endpoints = 'Magenta' + EndpointsHeader = 'Blue' + } +} +``` + +### **Match Dark Background Themes** + +Use colors that contrast well with dark themes: + +```powershell +@{ + Colors = @{ + Header = 'Gray' + Endpoints = 'BrightCyan' + OpenApiUrls = 'BrightYellow' + Divider = 'Gray' + } +} +``` + +### **Minimalist Setup** + +Reduce the use of vibrant colors for a subtle appearance: + +```powershell +@{ + Colors = @{ + Header = 'White' + Endpoints = 'White' + OpenApiUrls = 'White' + Divider = 'White' + } +} +``` + +### **Change Metrics Section Colors** + +```powershell +@{ + Colors = @{ + MetricsHeader = 'Blue' # Change the header color to Blue + MetricsLabel = 'Gray' # Use Gray for the value labels + MetricsValue = 'Red' # Display metric values in Red + } +} +``` diff --git a/docs/Getting-Started/Debug.md b/docs/Getting-Started/Debug.md index 0baef9a75..83303eca1 100644 --- a/docs/Getting-Started/Debug.md +++ b/docs/Getting-Started/Debug.md @@ -245,4 +245,4 @@ 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. +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. \ 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/docs/Hosting/IIS.md b/docs/Hosting/IIS.md index ee65ddf89..f0cc89a9c 100644 --- a/docs/Hosting/IIS.md +++ b/docs/Hosting/IIS.md @@ -337,7 +337,7 @@ Add-PodeAuthIIS -Name 'IISAuth' -ScriptBlock { ## IIS Advanced Kerberos -Kerberos Authentication can be configured using Active Directory account and +Kerberos Authentication can be configured using Active Directory account and Group Managed Service Account (gMSA) gMSA allows automatic password management, if you have more than 1 IIS server running Pode better to use gMSA for IIS AppPool Identity @@ -423,7 +423,7 @@ Start-PodeServer -StatusPageExceptions Show { ### Configuration steps for _Domain Account_: 1. Create Domain Users in AD for Pode AppPool - **Pode.Svc** -1. Create SPNs: +1. Create SPNs: ``` cmd setspn -d HTTP/PodeServer Contoso\pode.svc setspn -d HTTP/PodeServer.Contoso.com Contoso\pode.svc @@ -431,9 +431,9 @@ Start-PodeServer -StatusPageExceptions Show { 1. Configure **Pode.Svc** user Delegation - _Trust this user for delegation ..._ 1. Configure Pode Website to use **PodeServer.Contoso.com** as **Host Name** 1. Configure Pode Website AppPool to use _Contoso\pode.svc_ as **Identity** -1. Give write permissions to _Contoso\gmsaPodeSvc$_ on *Pode* folder +1. Give write permissions to _Contoso\gmsaPodeSvc$_ on *Pode* folder 1. _**!!! Important !!!**_ Add PTR DNS record _PodeServer.Contoso.com_ pointing to Load Balancer IP. If you have only one server and want to test, replace PTR record for _iis1.Contoso.com_ to _PodeServer.Contoso.com_ -1. Open URLs: +1. Open URLs: - https://PodeServer.Contoso.com/ - https://PodeServer.Contoso.com/test-kerberos - https://PodeServer.Contoso.com/test-kerberos-impersonation @@ -462,7 +462,7 @@ Start-PodeServer -StatusPageExceptions Show { # Reboot IIS servers to update hosts group membership! Restart-Computer -ComputerName "iis1","iis2" -force ``` -1. _**!!! Important !!!**_ Both IIS Servers must be rebooted to update Group Membership +1. _**!!! Important !!!**_ Both IIS Servers must be rebooted to update Group Membership 1. On both IIS Servers: ``` PowerShell Add-WindowsFeature RSAT-AD-PowerShell @@ -472,16 +472,16 @@ Start-PodeServer -StatusPageExceptions Show { ``` 1. Configure Pode Website to use **PodeServer.Contoso.com** as **Host Name** 1. Configure Pode Website AppPool to use _Contoso\gmsaPodeSvc$_ as **Identity** -1. Give write permissions to _Contoso\gmsaPodeSvc$_ on *Pode* folder +1. Give write permissions to _Contoso\gmsaPodeSvc$_ on *Pode* folder 1. _**!!! Important !!!**_ Add PTR DNS record _PodeServer.Contoso.com_ pointing to Load Balancer IP. If you have only one server and want to test, replace PTR record for _iis1.Contoso.com_ to _PodeServer.Contoso.com_ -1. Open URLs: +1. Open URLs: - https://PodeServer.Contoso.com/ - https://PodeServer.Contoso.com/test-kerberos - https://PodeServer.Contoso.com/test-kerberos-impersonation ### Kerberos Impersonate -Pode can impersonate the user that requests the web page using Kerberos Constrained Delegation (KCD). +Pode can impersonate the user that requests the web page using Kerberos Constrained Delegation (KCD). Requirements: @@ -503,7 +503,7 @@ To host your Pode server under IIS using Azure Web Apps, ensure the OS type is W Your web.config's `processPath` will also need to reference `powershell.exe` not `pwsh.exe`. -Pode can auto-detect if you're using an Azure Web App, but if you're having issues trying setting the `-DisableTermination` and `-Quiet` switches on your [`Start-PodeServer`](../../Functions/Core/Start-PodeServer). +Pode can auto-detect if you're using an Azure Web App, but if you're having issues trying setting the `-Daemon` switches on your [`Start-PodeServer`](../../Functions/Core/Start-PodeServer). ## Useful Links diff --git a/docs/Hosting/RunAsService.md b/docs/Hosting/RunAsService.md index f880dc63c..2872e7d0f 100644 --- a/docs/Hosting/RunAsService.md +++ b/docs/Hosting/RunAsService.md @@ -2,6 +2,9 @@ Rather than having to manually invoke your Pode server script each time, it's best if you can have it start automatically when your computer/server starts. Below you'll see how to set your script to run as either a Windows or a Linux service. +!!! Note + When running Pode as a service, it is recommended to use `Start-PodeServer` with the `-Daemon` parameter. This ensures the server operates in a detached and background-friendly mode suitable for long-running processes. The `-Daemon` parameter optimizes Pode's behavior for service execution by suppressing interactive output and allowing the process to run seamlessly in the background. + ## Windows To run your Pode server as a Windows service, we recommend using the [`NSSM`](https://nssm.cc) tool. To install on Windows you can use Chocolatey: diff --git a/docs/Tutorials/Authentication/Methods/OAuth2.md b/docs/Tutorials/Authentication/Methods/OAuth2.md index 4749bad3a..857ca051f 100644 --- a/docs/Tutorials/Authentication/Methods/OAuth2.md +++ b/docs/Tutorials/Authentication/Methods/OAuth2.md @@ -6,7 +6,7 @@ To use this scheme, you'll need to supply an Authorise/Token URL, as well as set ## Setup -Before using the OAuth2 authentication in Pode, you first need to register a new app within your service of choice. This registration will supply you with the required Client ID and Secret (if you're using [PKCE](#PKCE) then the Client Secret is optional). +Before using the OAuth2 authentication in Pode, you first need to register a new app within your service of choice. This registration will supply you with the required Client ID and Secret (if you're using [PKCE](#pkce) then the Client Secret is optional). To setup and start using OAuth2 authentication in Pode you use `New-PodeAuthScheme -OAuth2`, and then pipe this into the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function. diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index 6b18e04fa..f6a8720fa 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -46,6 +46,19 @@ Start-PodeServer { } ``` +### Custom Config Path + +Pode also allows you to explicitly specify a configuration file using the `-ConfigFile` parameter when starting the server. This is useful if you want to load a configuration file from a custom location. + +For example: + +```powershell +Start-PodeServer -ConfigFile "C:\Configs\custom_server.psd1" { + $port = (Get-PodeConfig).Port + Add-PodeEndpoint -Address * -Port $port -Protocol Http +} +``` + ## Environments Besides the default `server.psd1` file, Pode also supports environmental files based on the `$env:PODE_ENVIRONMENT` environment variable. @@ -69,7 +82,7 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: ``` | Path | Description | Docs | -| -------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +|----------------------------------|-----------------------------------------------------------------------------|-------------------------------------------------------------------------| | Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) | | Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) | | Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) | @@ -79,6 +92,7 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: | Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | | Server.ReceiveTimeout | Define the amount of time a Receive method call will block waiting for data | [link](../Endpoints/Basic/StaticContent/#server-timeout) | | Server.DefaultFolders | Set the Default Folders paths | [link](../Routes/Utilities/StaticContent/#changing-the-default-folders) | +| Server.Console | Set the Console settings | [link](../Getting-Started/Console) | | Web.OpenApi.DefaultDefinitionTag | Define the primary tag name for OpenAPI ( `default` is the default) | [link](../OpenAPI/Overview) | | Web.OpenApi.UsePodeYamlInternal | Force the use of the internal YAML converter (`False` is the default) | | | Web.Static.ValidateLast | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | @@ -86,4 +100,4 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: | Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | | Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | | Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | -| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | \ No newline at end of file +| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | diff --git a/docs/Tutorials/Events.md b/docs/Tutorials/Events.md index bd284a7f9..a44968ead 100644 --- a/docs/Tutorials/Events.md +++ b/docs/Tutorials/Events.md @@ -3,12 +3,19 @@ Pode lets you register scripts to be run when certain server events are triggered. The following types of events can have scripts registered: * Start +* Starting * Terminate +* Restarting * Restart * Browser * Crash * Stop * Running +* Suspending +* Suspend +* Resume +* Enable +* Disable And these events are triggered in the following order: @@ -44,10 +51,18 @@ If you need the runspaces to be opened, you'll want to look at the `Running` eve These scripts will also be re-invoked after a server restart has occurred. +### Starting + +Scripts registered to the `Starting` event will all be invoked during the initialization phase of the server, before the `Start` event is triggered. + ### Terminate Scripts registered to the `Terminate` event will all be invoked just before the server terminates. Ie, when the `Terminating...` message usually appears in the terminal, the script will run just after this and just before the `Done` message. Runspaces at this point will still be open. +### Restarting + +Scripts registered to the `Restarting` event will all be invoked when the server begins the restart process. This occurs before the `Restart` event. + ### Restart Scripts registered to the `Restart` event will all be invoked whenever an internal server restart occurs. This could be due to file monitoring, auto-restarting, `Ctrl+R`, or [`Restart-PodeServer`](../../Functions/Core/Restart-PodeServer). They will be invoked just after the `Restarting...` message appears in the terminal, and just before the `Done` message. Runspaces at this point will still be open. @@ -58,12 +73,33 @@ Scripts registered to the `Browser` event will all be invoked whenever the serve ### Crash -Scripts registered to the `Crash` event will all be invoked if the server ever terminates due to an exception being thrown. If a Crash event it triggered, then Terminate will not be triggered. Runspaces at this point will still be open, but there could be a chance not all of them will be available as the crash could have occurred from a runspace error. +Scripts registered to the `Crash` event will all be invoked if the server ever terminates due to an exception being thrown. If a Crash event is triggered, then Terminate will not be triggered. Runspaces at this point will still be open, but there could be a chance not all of them will be available as the crash could have occurred from a runspace error. ### Stop -Scripts registered to the `Stop` event will all be invoked when the server stops and closes. This event will be fired after either the Terminate or Crash events - which ever one causes the server to ultimately stop. Runspaces at this point will still be open. +Scripts registered to the `Stop` event will all be invoked when the server stops and closes. This event will be fired after either the Terminate or Crash events - whichever one causes the server to ultimately stop. Runspaces at this point will still be open. ### Running Scripts registered to the `Running` event will all be run soon after the `Start` event, even after a `Restart`. At this point all of the runspaces will have been opened and available for use. + +### Suspending + +Scripts registered to the `Suspending` event will all be invoked when the server begins the suspension process. + +### Suspend + +Scripts registered to the `Suspend` event will all be invoked when the server completes the suspension process. + +### Resume + +Scripts registered to the `Resume` event will all be invoked when the server resumes operation after suspension. + +### Enable + +Scripts registered to the `Enable` event will all be invoked when the server is enabled. + +### Disable + +Scripts registered to the `Disable` event will all be invoked when the server is disabled. + diff --git a/docs/Tutorials/Server Operations/Disabling/Overview.md b/docs/Tutorials/Server Operations/Disabling/Overview.md new file mode 100644 index 000000000..6c6ce215b --- /dev/null +++ b/docs/Tutorials/Server Operations/Disabling/Overview.md @@ -0,0 +1,59 @@ +# Overview + +Pode provides a way to control the availability of the server for new incoming requests. Using the **Disable** and **Enable** operations, you can temporarily block or allow client requests while maintaining the server's state. These features are particularly useful for maintenance, load management, or during planned downtime. + +--- + +## Disabling the Server + +The **Disable** operation blocks new incoming requests to the Pode server by integrating middleware that returns a `503 Service Unavailable` response to clients. This is ideal for situations where you need to prevent access temporarily without fully stopping the server. + +### How It Works + +1. **Blocking Requests**: + - All new incoming requests are intercepted and responded to with a `503 Service Unavailable` status. + - A `Retry-After` header is included in the response, advising clients when they can attempt to reconnect. + +2. **Customizing Retry Time**: + - The retry time (in seconds) can be customized to specify when the service is expected to become available again. By default, this is set to **1 hour**. + +### Example Usage + +```powershell +# Block new requests with the default retry time (1 hour) +Disable-PodeServer + +# Block new requests with a custom retry time (5 minutes) +Disable-PodeServer -RetryAfter 300 +``` + +--- + +## Enabling the Server + +The **Enable** operation restores the Pode server's ability to accept new incoming requests by removing the middleware that blocks them. This is useful after completing maintenance or resolving an issue. + +### How It Works + +1. **Resetting the Cancellation Token**: + - The server's internal state is updated to resume normal operations, allowing new incoming requests to be processed. + +2. **Restoring Functionality**: + - Once enabled, the Pode server can handle requests as usual, with no further interruptions. + +### Example Usage + +```powershell +# Enable the Pode server to resume accepting requests +Enable-PodeServer +``` + +--- + +## When to Use Disable and Enable + +These operations are particularly useful for: + +- **Planned Downtime**: Prevent client access during scheduled maintenance or updates. +- **Load Management**: Temporarily block requests during high traffic to alleviate pressure on the server. +- **Testing**: Control the server's availability during development or troubleshooting scenarios. diff --git a/docs/Tutorials/Server Operations/Overview.md b/docs/Tutorials/Server Operations/Overview.md new file mode 100644 index 000000000..4f52e3ee6 --- /dev/null +++ b/docs/Tutorials/Server Operations/Overview.md @@ -0,0 +1,81 @@ +# Overview + +Pode offers a suite of server management operations to provide granular control over server behavior. These operations include **suspending**, **resuming**, **restarting**, and **disabling/enabling** the server. Each operation is designed to address specific use cases, such as debugging, maintenance, or load management, ensuring the server can adapt to dynamic requirements without a complete shutdown. + +### Learn More About Server Operations + +- [**Suspending**](./Suspending/Overview.md): Temporarily pause server activities and later restore them without a full restart. +- [**Restarting**](./Restarting/Overview.md): Multiple ways to restart the server, including file monitoring, scheduled restarts, and manual commands. +- [**Disabling**](./Disabling/Overview.md): Block or allow new incoming requests without affecting the server’s state. + +--- + +## Configuring Allowed Actions + +Pode introduces the ability to configure and control server behaviors using the `AllowedActions` section in the `server.psd1` configuration file. This feature enables fine-grained management of server operations, including suspend, restart, and disable functionality. + +### Default Configuration + +```powershell +@{ + Server = @{ + AllowedActions = @{ + Suspend = $true # Enable or disable the suspend operation + Restart = $true # Enable or disable the restart operation + Disable = $true # Enable or disable the disable operation + DisableSettings = @{ + RetryAfter = 3600 # Default retry time (in seconds) for Disable-PodeServer + MiddlewareName = '__Pode_Midleware_Code_503__' # Name of the middleware scriptblock + } + Timeout = @{ + Suspend = 30 # Maximum seconds to wait before suspending + Resume = 30 # Maximum seconds to wait before resuming + } + } + } +} +``` + +### Key Configuration Options + +1. **Suspend**: Enables or disables the ability to suspend the server via `Suspend-PodeServer`. +2. **Restart**: Allows you to enable or disable server restarts. +3. **Disable**: Controls whether the server can block new incoming requests using `Disable-PodeServer`. +4. **DisableSettings**: + - `RetryAfter`: Specifies the default retry time (in seconds) included in the `Retry-After` header when the server is disabled. + - `MiddlewareName`: Defines the name of the middleware scriptblock responsible for handling `503 Service Unavailable` responses. +5. **Timeout**: + - `Suspend`: Defines the maximum wait time (in seconds) for runspaces to suspend. + - `Resume`: Defines the maximum wait time (in seconds) for runspaces to resume. + +### Benefits of Allowed Actions + +- **Customizable Behavior**: Tailor server operations to match your application’s requirements. +- **Enhanced Control**: Prevent unwanted actions like suspending or restarting during critical operations. +- **Predictability**: Enforce consistent timeouts for suspend and resume actions to avoid delays. +- **Middleware Control**: Specify a custom middleware scriptblock for handling specific scenarios, such as client retries during downtime. + +--- + +## Monitoring the Server State + +In addition to managing operations, Pode allows you to monitor the server's state using the `Get-PodeServerState` function. This command evaluates the server’s internal state and returns a status such as: + +- **Starting**: The server is initializing. +- **Running**: The server is actively processing requests. +- **Suspending**: The server is pausing all activities. +- **Suspended**: The server has paused all activities and is not accepting new requests. +- **Resuming**: The server is restoring paused activities to normal operation. +- **Restarting**: The server is in the process of restarting. +- **Terminating**: The server is shutting down gracefully. +- **Terminated**: The server has fully stopped. + +### Example Usage + +```powershell +# Retrieve the current state of the Pode server +$state = Get-PodeServerState +Write-Output "The server is currently: $state" +``` + +The `Get-PodeServerState` function is especially useful for integrating server monitoring into automated workflows or debugging complex operations. diff --git a/docs/Tutorials/Restarting/Overview.md b/docs/Tutorials/Server Operations/Restarting/Overview.md similarity index 100% rename from docs/Tutorials/Restarting/Overview.md rename to docs/Tutorials/Server Operations/Restarting/Overview.md diff --git a/docs/Tutorials/Restarting/Types/AutoRestarting.md b/docs/Tutorials/Server Operations/Restarting/Type/AutoRestarting.md similarity index 100% rename from docs/Tutorials/Restarting/Types/AutoRestarting.md rename to docs/Tutorials/Server Operations/Restarting/Type/AutoRestarting.md diff --git a/docs/Tutorials/Restarting/Types/FileMonitoring.md b/docs/Tutorials/Server Operations/Restarting/Type/FileMonitoring.md similarity index 100% rename from docs/Tutorials/Restarting/Types/FileMonitoring.md rename to docs/Tutorials/Server Operations/Restarting/Type/FileMonitoring.md diff --git a/docs/Tutorials/Server Operations/Suspending/Overview.md b/docs/Tutorials/Server Operations/Suspending/Overview.md new file mode 100644 index 000000000..3b8777d6b --- /dev/null +++ b/docs/Tutorials/Server Operations/Suspending/Overview.md @@ -0,0 +1,54 @@ +# 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-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-PodeServer` from within your Pode environment or script. + + ```powershell + 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-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. + +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-PodeServer`. This will restore the Pode server to its normal operational state: + +1. **Run the Resume Command**: + - Call `Resume-PodeServer` to bring the server back online. + + ```powershell + Resume-PodeServer + ``` + +2. **Resumption Process**: + - 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. + +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/docs/Tutorials/Tasks.md b/docs/Tutorials/Tasks.md index 82743fda9..9eb30c13b 100644 --- a/docs/Tutorials/Tasks.md +++ b/docs/Tutorials/Tasks.md @@ -16,7 +16,7 @@ Add-PodeTask -Name 'Example' -ScriptBlock { } ``` -A task's scriptblock can also return values, that can be retrieved later on (see [Invoking](#Invoking)): +A task's scriptblock can also return values, that can be retrieved later on (see [Invoking](#invoking)): ```powershell Add-PodeTask -Name 'Example' -ScriptBlock { @@ -159,6 +159,7 @@ You normally define a task's script using the `-ScriptBlock` parameter however, For example, to create a task from a file that will output `Hello, world`: * File.ps1 + ```powershell { 'Hello, world!' | Out-PodeHost @@ -166,6 +167,7 @@ For example, to create a task from a file that will output `Hello, world`: ``` * Task + ```powershell Add-PodeTask -Name 'from-file' -FilePath './Tasks/File.ps1' ``` 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/examples/FileBrowser/FileBrowser.ps1 b/examples/FileBrowser/FileBrowser.ps1 index da79d9a40..54273c331 100644 --- a/examples/FileBrowser/FileBrowser.ps1 +++ b/examples/FileBrowser/FileBrowser.ps1 @@ -41,7 +41,7 @@ catch { throw } $directoryPath = $podePath # Start Pode server -Start-PodeServer -ScriptBlock { +Start-PodeServer -ConfigFile '..\Server.psd1' -ScriptBlock { Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Default diff --git a/examples/HelloWorld/HelloWorld.ps1 b/examples/HelloWorld/HelloWorld.ps1 index 949e00c22..9c4d5a417 100644 --- a/examples/HelloWorld/HelloWorld.ps1 +++ b/examples/HelloWorld/HelloWorld.ps1 @@ -44,7 +44,7 @@ catch { # Import-Module Pode # Start the Pode server -Start-PodeServer { +Start-PodeServer -ConfigFile '..\Server.psd1' { # Add an HTTP endpoint listening on localhost at port 8080 Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http diff --git a/examples/OpenApi-TuttiFrutti.ps1 b/examples/OpenApi-TuttiFrutti.ps1 index 778597aef..e80ff8023 100644 --- a/examples/OpenApi-TuttiFrutti.ps1 +++ b/examples/OpenApi-TuttiFrutti.ps1 @@ -13,11 +13,12 @@ .PARAMETER PortV3_1 The port on which the Pode server will listen for OpenAPI v3_1. Default is 8081. -.PARAMETER Quiet - Suppresses output when the server is running. +.PARAMETER Daemon + Configures the server to run as a daemon with minimal console interaction and output. -.PARAMETER DisableTermination - Prevents the server from being terminated. +.PARAMETER IgnoreServerConfig + Ignores the server.psd1 configuration file when starting the server. + This parameter ensures the server does not load or apply any settings defined in the server.psd1 file, allowing for a fully manual configuration at runtime. .EXAMPLE To run the sample: ./OpenApi-TuttiFrutti.ps1 @@ -45,10 +46,10 @@ param( $PortV3_1 = 8081, [switch] - $Quiet, + $Daemon, [switch] - $DisableTermination + $IgnoreServerConfig ) try { @@ -66,9 +67,9 @@ try { } catch { throw } -Start-PodeServer -Threads 1 -Quiet:$Quiet -DisableTermination:$DisableTermination -ScriptBlock { +Start-PodeServer -Threads 1 -Daemon:$Daemon -IgnoreServerConfig:$IgnoreServerConfig -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $PortV3 -Protocol Http -Default -Name 'endpoint_v3' - Add-PodeEndpoint -Address localhost -Port $PortV3_1 -Protocol Http -Default -Name 'endpoint_v3.1' + Add-PodeEndpoint -Address localhost -Port $PortV3_1 -Protocol Http -Name 'endpoint_v3.1' New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging $InfoDescription = @' This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about Swagger at [http://swagger.io](http://swagger.io). @@ -422,7 +423,7 @@ Some useful links: Select-PodeOADefinition -Tag 'v3' -Scriptblock { Add-PodeRouteGroup -Path '/api/v3/private' -Routes { - Add-PodeRoute -PassThru -Method Put,Post -Path '/pat/:petId' -ScriptBlock { + Add-PodeRoute -PassThru -Method Put, Post -Path '/pat/:petId' -ScriptBlock { $JsonPet = ConvertTo-Json $WebEvent.data if ( Update-Pet -Id $WebEvent.Parameters['petId'] -Data $JsonPet) { Write-PodeJsonResponse -Value @{} -StatusCode 200 diff --git a/examples/PetStore/Petstore-OpenApiMultiTag.ps1 b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 index 5ffcea4e7..3e5c6ca91 100644 --- a/examples/PetStore/Petstore-OpenApiMultiTag.ps1 +++ b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 @@ -96,12 +96,12 @@ Start-PodeServer -Threads 1 -ScriptBlock { $Certificate = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).Certificate $CertificateKey = Join-Path -Path $CertsPath -ChildPath (Get-PodeConfig).CertificateKey Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Default -Name 'endpoint_v3' - Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port ((Get-PodeConfig).RestFulPort + 1) -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Default -Name 'endpoint_v3.1' + Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port ((Get-PodeConfig).RestFulPort + 1) -Protocol Https -Certificate $Certificate -CertificateKey $CertificateKey -CertificatePassword (Get-PodeConfig).CertificatePassword -Name 'endpoint_v3.1' } } else { Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port (Get-PodeConfig).RestFulPort -Protocol Http -Default -Name 'endpoint_v3' - Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port ((Get-PodeConfig).RestFulPort + 1) -Protocol Http -Default -Name 'endpoint_v3.1' + Add-PodeEndpoint -Address (Get-PodeConfig).Address -Port ((Get-PodeConfig).RestFulPort + 1) -Protocol Http -Name 'endpoint_v3.1' } New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging diff --git a/examples/PetStore/data/PetData.json b/examples/PetStore/data/PetData.json index dfee685c6..1f5fe3cd8 100644 --- a/examples/PetStore/data/PetData.json +++ b/examples/PetStore/data/PetData.json @@ -360,4 +360,4 @@ "Users" ] } -} +} \ No newline at end of file diff --git a/examples/Suspend-Resume.ps1 b/examples/Suspend-Resume.ps1 new file mode 100644 index 000000000..d045e9060 --- /dev/null +++ b/examples/Suspend-Resume.ps1 @@ -0,0 +1,221 @@ +<# +.SYNOPSIS + Initializes and starts a Pode server with OpenAPI support, scheduling, and error logging. + +.DESCRIPTION + This script sets up a Pode server with multiple features including: + - HTTP, HTTPS, TCP, SMTP, and WebSocket endpoints. + - OpenAPI documentation and viewers for API specifications. + - Error logging to the terminal. + - Task and schedule management. + - Signal and verb-based routes for custom protocols. + - Session management. + + It demonstrates various functionalities provided by Pode, including OpenAPI integrations, signal routes, and scheduled tasks. + +.PARAMETER ScriptPath + Specifies the path to the current script. + +.PARAMETER PodePath + Specifies the path to the Pode module. If the module is available in the source path, it is loaded from there; otherwise, it is loaded from installed modules. + +.EXAMPLE + To run the sample: + ./Suspend-Resume.ps1 + + OpenAPI Info: + - Specification: http://localhost:8081/openapi + - Documentation: http://localhost:8081/docs + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/Suspend-Resume.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 4 -EnablePool Tasks -IgnoreServerConfig -ScriptBlock { + + # listen on localhost:8081 + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Name 'General' -Default + Add-PodeEndpoint -Address localhost -Port 8082 -Protocol Https -SelfSigned + Add-PodeEndpoint -Address localhost -Port 8083 -Protocol Http -DualMode + Add-PodeEndpoint -Address localhost -Port 8025 -Protocol Smtp + Add-PodeEndpoint -Address localhost -Port 8026 -Protocol Smtps -SelfSigned + Add-PodeEndpoint -Address localhost -Port 8091 -Protocol Ws -Name 'WS1' + Add-PodeEndpoint -Address localhost -Port 8093 -Protocol Wss -SelfSigned + 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-PodeTaskConcurrency -Maximum 10 + + # set view engine to pode renderer + Set-PodeViewEngine -Type Html + + # 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 -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' + 'here' + } + + # setup an smtp handler + Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { + 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-Verbose '|' + $SmtpEvent.Email + $SmtpEvent.Request + $SmtpEvent.Email.Headers + Write-Verbose '- - - - - - - - - - - - - - - - - -' + } + + # 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 } + } + + 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)!" + } + + + Add-PodeTask -Name 'Test' -ScriptBlock { + param($value) + Start-PodeSleep -Seconds 10 + Write-Verbose "a $($value) is comming" + Start-PodeSleep -Seconds 10 + Write-Verbose "a $($value) is comming...2" + Start-PodeSleep -Seconds 10 + Write-Verbose "a $($value) is comming...3" + Start-PodeSleep -Seconds 10 + Write-Verbose "a $($value) is comming...4" + Start-PodeSleep -Seconds 10 + Write-Verbose "a $($value) is comming...5" + Start-PodeSleep -Seconds 10 + Write-Verbose "a $($value) is comming...6" + Start-PodeSleep -Seconds 10 + 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) + 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)" + } + + # schedule defined using two cron expressions + Add-PodeSchedule -Name 'two-crons' -Cron @('0/3 * * * *', '0/5 * * * *') -ScriptBlock { + Write-Verbose 'double cron' + Get-PodeSchedule -Name 'two-crons' | Write-Verbose + } + + # 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/examples/SwaggerEditor/Swagger-Editor.ps1 b/examples/SwaggerEditor/Swagger-Editor.ps1 index 175345172..80aecdd3e 100644 --- a/examples/SwaggerEditor/Swagger-Editor.ps1 +++ b/examples/SwaggerEditor/Swagger-Editor.ps1 @@ -42,7 +42,7 @@ catch { throw } # Import-Module Pode # create a server, and start listening on port 8081 -Start-PodeServer -Threads 2 { +Start-PodeServer -ConfigFile '..\Server.psd1' -Threads 2 { # listen on localhost:8081 Add-PodeEndpoint -Address localhost -Port $port -Protocol Http diff --git a/examples/server.psd1 b/examples/server.psd1 index d1858842e..b5f36daf4 100644 --- a/examples/server.psd1 +++ b/examples/server.psd1 @@ -29,11 +29,11 @@ } } Server = @{ - FileMonitor = @{ + FileMonitor = @{ Enable = $false ShowFiles = $true } - Logging = @{ + Logging = @{ Masking = @{ Patterns = @( '(?Password=)\w+', @@ -42,7 +42,7 @@ Mask = '--MASKED--' } } - AutoImport = @{ + AutoImport = @{ Functions = @{ ExportOnly = $true } @@ -55,14 +55,72 @@ } } } - Request = @{ + Request = @{ Timeout = 30 BodySize = 100MB } - Debug = @{ + Debug = @{ Breakpoints = @{ Enable = $true } } + AllowedActions = @{ + Suspend = $true # Enable or disable the suspend operation + Restart = $true # Enable or disable the restart operation + Disable = $true # Enable or disable the disable operation + DisableSettings = @{ + RetryAfter = 3600 # Default retry time (in seconds) for Disable-PodeServer + MiddlewareName = '__Pode_Midleware_Code_503' # Name of the middleware scriptblock + } + Timeout = @{ + Suspend = 30 # Maximum seconds to wait before suspending + Resume = 30 # Maximum seconds to wait before resuming + } + } + Console = @{ + DisableTermination = $false # Prevent Ctrl+C from terminating the server. + DisableConsoleInput = $false # Disable all console input controls. + Quiet = $false # Suppress console output. + ClearHost = $false # Clear the console output at startup. + ShowOpenAPI = $true # Display OpenAPI information. + ShowEndpoints = $true # Display listening endpoints. + ShowHelp = $false # Show help instructions in the console. + ShowDivider = $true # Display dividers between sections. + DividerLength = 75 # Length of dividers in the console. + ShowTimeStamp = $true # Display timestamp in the header. + Colors = @{ # Customize console colors. + Header = 'White' # The server's header section, including the Pode version and timestamp. + EndpointsHeader = 'Yellow' # The header for the endpoints list. + Endpoints = 'Cyan' # The endpoints URLs. + EndpointsProtocol = 'White' # The endpoints protocol. + EndpointsFlag = 'Gray' # The endpoints flags. + EndpointsName = 'Magenta' # The endpoints name. + OpenApiUrls = 'Cyan' # URLs listed under the OpenAPI information section. + OpenApiHeaders = 'Yellow' # Section headers for OpenAPI information. + OpenApiTitles = 'White' # The OpenAPI "default" title. + OpenApiSubtitles = 'Yellow' # Subtitles under OpenAPI (e.g., Specification, Documentation). + HelpHeader = 'Yellow' # Header for the Help section. + HelpKey = 'Green' # Key bindings listed in the Help section (e.g., Ctrl+c). + HelpDescription = 'White' # Descriptions for each Help section key binding. + HelpDivider = 'Gray' # Dividers used in the Help section. + Divider = 'DarkGray' # Dividers between console sections. + MetricsHeader = 'Yellow' # Header for the Metric section. + MetricsLabel = 'White' # Labels for values displayed in the Metrics section. + MetricsValue = 'Green' # The actual values displayed in the Metrics section. + } + KeyBindings = @{ # Define custom key bindings for controls. Refer to https://learn.microsoft.com/en-us/dotnet/api/system.consolekey?view=net-9.0 + Browser = 'B' # Open the default browser. + Help = 'H' # Show/hide help instructions. + OpenAPI = 'O' # Show/hide OpenAPI information. + Endpoints = 'E' # Show/hide endpoints. + Clear = 'L' # Clear the console output. + Quiet = 'Q' # Toggle quiet mode. + Terminate = 'C' # Terminate the server. + Restart = 'R' # Restart the server. + Disable = 'D' # Disable the server. + Suspend = 'P' # Suspend the server. + Metrics = 'M' # Show Metrics. + } + } } } \ No newline at end of file diff --git a/src/Listener/PodeNative.cs b/src/Listener/PodeNative.cs new file mode 100644 index 000000000..c961870e7 --- /dev/null +++ b/src/Listener/PodeNative.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; + +namespace Pode +{ + public static class NativeMethods + { + // Constants for standard Windows handles + public const int STD_INPUT_HANDLE = -10; + public const int STD_OUTPUT_HANDLE = -11; + public const int STD_ERROR_HANDLE = -12; + + + // Constants for standard UNIX file descriptors + public const int STDIN_FILENO = 0; + public const int STDOUT_FILENO = 1; + public const int STDERR_FILENO = 2; + + + // Import the GetStdHandle function from kernel32.dll + [DllImport("kernel32.dll", SetLastError = true)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "SYSLIB1054:Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time", Justification = "")] + private static extern IntPtr GetStdHandle(int nStdHandle); + + // Helper method to check if a handle is valid + public static bool IsHandleValid(int handleType) + { + IntPtr handle = GetStdHandle(handleType); + return handle != IntPtr.Zero; + } + + + // Import the isatty function from libc + [DllImport("libc")] + private static extern int isatty(int fd); + + // Method to check if a file descriptor is a terminal + public static bool IsTerminal(int fileDescriptor) + { + return isatty(fileDescriptor) == 1; + } + } + +} diff --git a/src/Listener/PodeServerEventType.cs b/src/Listener/PodeServerEventType.cs new file mode 100644 index 000000000..d7ce20c86 --- /dev/null +++ b/src/Listener/PodeServerEventType.cs @@ -0,0 +1,84 @@ +namespace Pode +{ + /// + /// Represents the types of events a Pode server can trigger or respond to. + /// + public enum PodeServerEventType + { + /// + /// Triggered when the server starts. + /// + Start, + + /// + /// Triggered when the server is initializing. + /// + Starting, + + /// + /// Triggered when the server terminates. + /// + Terminate, + + /// + /// Triggered when the server begins the restart process. + /// + Restarting, + + /// + /// Triggered when the server completes the restart process. + /// + Restart, + + /// + /// Triggered when a user opens a web page pointing to the first HTTP/HTTPS endpoint. + /// + Browser, + + /// + /// Triggered when the server crashes unexpectedly. + /// + Crash, + + /// + /// Triggered when the server stops. + /// + Stop, + + /// + /// Indicates that the server is running (retained for backward compatibility). + /// + Running, + + /// + /// Triggered when the server begins the suspension process. + /// + Suspending, + + /// + /// Triggered when the server completes the suspension process. + /// + Suspend, + + /// + /// Triggered when the server begins the resume process. + /// + /// + Resuming, + + /// + /// Triggered when the server resumes operation after suspension. + /// + Resume, + + /// + /// Triggered when the server is enabled. + /// + Enable, + + /// + /// Triggered when the server is disabled. + /// + Disable + } +} diff --git a/src/Listener/PodeServerState.cs b/src/Listener/PodeServerState.cs new file mode 100644 index 000000000..9549137ff --- /dev/null +++ b/src/Listener/PodeServerState.cs @@ -0,0 +1,51 @@ +/// +/// Represents the various states a Pode server can be in. +/// +namespace Pode +{ + /// + /// Enum for defining the states of the Pode server. + /// + public enum PodeServerState + { + /// + /// The server has been completely terminated and is no longer running. + /// + Terminated, + + /// + /// The server is in the process of terminating and shutting down its operations. + /// + Terminating, + + /// + /// The server is resuming from a suspended state and is starting to run again. + /// + Resuming, + + /// + /// The server is in the process of suspending its operations. + /// + Suspending, + + /// + /// The server is currently suspended and not processing any requests. + /// + Suspended, + + /// + /// The server is in the process of restarting its operations. + /// + Restarting, + + /// + /// The server is starting its operations. + /// + Starting, + + /// + /// The server is running and actively processing requests. + /// + Running + } +} diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 1c7ff7daf..c7e81c93c 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'لم يتم العثور على المسار لتحميل {0}: {1}' failedToImportModuleExceptionMessage = 'فشل في استيراد الوحدة: {0}' endpointNotExistExceptionMessage = "نقطة النهاية مع البروتوكول '{0}' والعنوان '{1}' أو العنوان المحلي '{2}' غير موجودة." - terminatingMessage = 'إنهاء...' + terminatingMessage = 'إنهاء' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'لم يتم توفير أي أوامر لتحويلها إلى طرق.' invalidTaskTypeExceptionMessage = 'نوع المهمة غير صالح، المتوقع إما [System.Threading.Tasks.Task] أو [hashtable].' alreadyConnectedToWebSocketExceptionMessage = "متصل بالفعل بـ WebSocket بالاسم '{0}'" @@ -291,5 +291,35 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' لا يمكن أن يحتوي على جسم الطلب. استخدم -AllowNonStandardBody لتجاوز هذا التقييد." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' - LocalEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API." + localEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API." + suspendingMessage = 'تعليق' + resumingMessage = 'استئناف' + serverControlCommandsTitle = 'أوامر التحكم بالخادم:' + gracefullyTerminateMessage = 'إنهاء الخادم بلطف.' + restartServerMessage = 'إعادة تشغيل الخادم وتحميل التكوينات.' + resumeServerMessage = 'استئناف الخادم.' + suspendServerMessage = 'تعليق الخادم.' + startingMessage = 'جارٍ البدء' + restartingMessage = 'جارٍ إعادة التشغيل' + suspendedMessage = 'معلق' + runningMessage = 'يعمل' + openHttpEndpointMessage = 'افتح أول نقطة نهاية HTTP في المتصفح الافتراضي.' + terminatedMessage = 'تم الإنهاء' + showMetricsMessage = 'عرض المقاييس' + clearConsoleMessage = 'مسح وحدة التحكم' + serverMetricsMessage = 'مقاييس الخادم' + totalUptimeMessage = 'إجمالي وقت التشغيل:' + uptimeSinceLastRestartMessage = 'وقت التشغيل منذ آخر إعادة تشغيل:' + totalRestartMessage = 'إجمالي عدد عمليات إعادة التشغيل:' + defaultEndpointAlreadySetExceptionMessage = "تم تعيين نقطة نهاية افتراضية للنوع '{0}'. يُسمح فقط بنقطة نهاية افتراضية واحدة لكل نوع." + enableHttpServerMessage = 'تمكين خادم HTTP' + disableHttpServerMessage = 'تعطيل خادم HTTP' + showHelpMessage = 'عرض المساعدة' + hideHelpMessage = 'إخفاء المساعدة' + hideEndpointsMessage = 'إخفاء نقاط النهاية' + showEndpointsMessage = 'عرض نقاط النهاية' + hideOpenAPIMessage = 'إخفاء OpenAPI' + showOpenAPIMessage = 'عرض OpenAPI' + enableQuietModeMessage = 'تمكين الوضع الصامت' + disableQuietModeMessage = 'تعطيل الوضع الصامت' } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index c90847e01..5bdd32340 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'Pfad zum Laden von {0} nicht gefunden: {1}' failedToImportModuleExceptionMessage = 'Modulimport fehlgeschlagen: {0}' endpointNotExistExceptionMessage = "Der Endpunkt mit dem Protokoll '{0}' und der Adresse '{1}' oder der lokalen Adresse '{2}' existiert nicht" - terminatingMessage = 'Beenden...' + terminatingMessage = 'Beenden' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Keine Befehle zur Umwandlung in Routen bereitgestellt.' invalidTaskTypeExceptionMessage = 'Aufgabentyp ist ungültig, erwartet entweder [System.Threading.Tasks.Task] oder [hashtable]' alreadyConnectedToWebSocketExceptionMessage = "Bereits mit dem WebSocket mit dem Namen '{0}' verbunden" @@ -291,5 +291,35 @@ 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 = 'Anhalten' + resumingMessage = 'Fortsetzen' + serverControlCommandsTitle = 'Serversteuerbefehle:' + gracefullyTerminateMessage = 'Server sanft beenden.' + restartServerMessage = 'Server neu starten und Konfigurationen laden.' + resumeServerMessage = 'Server fortsetzen.' + suspendServerMessage = 'Server anhalten.' + startingMessage = 'Starten' + restartingMessage = 'Neustart' + suspendedMessage = 'Angehalten' + runningMessage = 'Läuft' + openHttpEndpointMessage = 'Öffnen Sie den ersten HTTP-Endpunkt im Standardbrowser.' + terminatedMessage = 'Beendet' + showMetricsMessage = 'Metriken anzeigen' + clearConsoleMessage = 'Konsole löschen' + serverMetricsMessage = 'Servermetriken' + totalUptimeMessage = 'Gesamtlaufzeit:' + uptimeSinceLastRestartMessage = 'Laufzeit seit dem letzten Neustart:' + totalRestartMessage = 'Gesamtanzahl der Neustarts:' + defaultEndpointAlreadySetExceptionMessage = "Ein Standardendpunkt für den Typ '{0}' ist bereits festgelegt. Pro Typ ist nur ein Standardendpunkt erlaubt." + enableHttpServerMessage = 'HTTP-Server aktivieren' + disableHttpServerMessage = 'HTTP-Server deaktivieren' + showHelpMessage = 'Hilfe anzeigen' + hideHelpMessage = 'Hilfe ausblenden' + hideEndpointsMessage = 'Endpoints ausblenden' + showEndpointsMessage = 'Endpoints anzeigen' + hideOpenAPIMessage = 'OpenAPI ausblenden' + showOpenAPIMessage = 'OpenAPI anzeigen' + enableQuietModeMessage = 'Leisemodus aktivieren' + disableQuietModeMessage = 'Leisemodus deaktivieren' } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 56340e18f..e11f1a408 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -9,7 +9,7 @@ noLogicPassedForRouteExceptionMessage = 'No logic passed for Route: {0}' scriptPathDoesNotExistExceptionMessage = 'The script path does not exist: {0}' mutexAlreadyExistsExceptionMessage = 'A mutex with the following name already exists: {0}' - listeningOnEndpointsMessage = 'Listening on the following {0} endpoint(s) [{1} thread(s)]:' + listeningOnEndpointsMessage = 'Listening on {0} endpoint(s) [{1} thread(s)]:' unsupportedFunctionInServerlessContextExceptionMessage = 'The {0} function is not supported in a serverless context.' expectedNoJwtSignatureSuppliedExceptionMessage = 'Expected no JWT signature to be supplied.' secretAlreadyMountedExceptionMessage = "A Secret with the name '{0}' has already been mounted." @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'Path to load {0} not found: {1}' failedToImportModuleExceptionMessage = 'Failed to import module: {0}' endpointNotExistExceptionMessage = "Endpoint with protocol '{0}' and address '{1}' or local address '{2}' does not exist." - terminatingMessage = 'Terminating...' + terminatingMessage = 'Terminating' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'No commands supplied to convert to Routes.' invalidTaskTypeExceptionMessage = 'Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable]' alreadyConnectedToWebSocketExceptionMessage = "Already connected to WebSocket with name '{0}'" @@ -277,7 +277,7 @@ discriminatorIncompatibleWithAllOfExceptionMessage = "The parameter 'Discriminator' is incompatible with 'allOf'." noNameForWebSocketSendMessageExceptionMessage = 'No Name for a WebSocket to send message to supplied.' hashtableMiddlewareNoLogicExceptionMessage = 'A Hashtable Middleware supplied has no Logic defined.' - openApiInfoMessage = 'OpenAPI Info:' + openApiInfoMessage = 'OpenAPI Information:' invalidSchemeForAuthValidatorExceptionMessage = "The supplied '{0}' Scheme for the '{1}' authentication validator requires a valid ScriptBlock." sseFailedToBroadcastExceptionMessage = 'SSE failed to broadcast due to defined SSE broadcast level for {0}: {1}' adModuleWindowsOnlyExceptionMessage = 'Active Directory module only available on Windows OS.' @@ -291,4 +291,35 @@ 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' + resumingMessage = 'Resuming' + serverControlCommandsTitle = 'Server Control Commands:' + gracefullyTerminateMessage = 'Gracefully terminate the server.' + restartServerMessage = 'Restart the server and reload configurations.' + resumeServerMessage = 'Resume the server.' + suspendServerMessage = 'Suspend the server.' + startingMessage = 'Starting' + restartingMessage = 'Restarting' + suspendedMessage = 'Suspended' + runningMessage = 'Running' + openHttpEndpointMessage = 'Open the default HTTP endpoint in the default browser.' + terminatedMessage = 'Terminated' + showMetricsMessage = 'Show Metrics' + clearConsoleMessage = 'Clear the Console' + serverMetricsMessage = 'Server Metrics' + totalUptimeMessage = 'Total Uptime:' + uptimeSinceLastRestartMessage = 'Uptime Since Last Restart:' + totalRestartMessage = 'Total Number of Restarts:' + defaultEndpointAlreadySetExceptionMessage = "A default endpoint for the type '{0}' is already set. Only one default endpoint is allowed per type." + enableHttpServerMessage = 'Enable HTTP Server' + disableHttpServerMessage = 'Disable HTTP Server' + showHelpMessage = 'Show Help' + hideHelpMessage = 'Hide Help' + hideEndpointsMessage = 'Hide Endpoints' + showEndpointsMessage = 'Show Endpoints' + hideOpenAPIMessage = 'Hide OpenAPI' + showOpenAPIMessage = 'Show OpenAPI' + enableQuietModeMessage = 'Enable Quiet Mode' + disableQuietModeMessage = 'Disable Quiet Mode' +} \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 44a1ee102..755124e8f 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -9,7 +9,7 @@ noLogicPassedForRouteExceptionMessage = 'No logic passed for Route: {0}' scriptPathDoesNotExistExceptionMessage = 'The script path does not exist: {0}' mutexAlreadyExistsExceptionMessage = 'A mutex with the following name already exists: {0}' - listeningOnEndpointsMessage = 'Listening on the following {0} endpoint(s) [{1} thread(s)]:' + listeningOnEndpointsMessage = 'Listening on {0} endpoint(s) [{1} thread(s)]:' unsupportedFunctionInServerlessContextExceptionMessage = 'The {0} function is not supported in a serverless context.' expectedNoJwtSignatureSuppliedExceptionMessage = 'Expected no JWT signature to be supplied.' secretAlreadyMountedExceptionMessage = "A Secret with the name '{0}' has already been mounted." @@ -28,7 +28,7 @@ pathToLoadNotFoundExceptionMessage = 'Path to load {0} not found: {1}' failedToImportModuleExceptionMessage = 'Failed to import module: {0}' endpointNotExistExceptionMessage = "Endpoint with protocol '{0}' and address '{1}' or local address '{2}' does not exist." - terminatingMessage = 'Terminating...' + terminatingMessage = 'Terminating' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'No commands supplied to convert to Routes.' invalidTaskTypeExceptionMessage = 'Task type is invalid, expected either [System.Threading.Tasks.Task] or [hashtable]' alreadyConnectedToWebSocketExceptionMessage = "Already connected to WebSocket with name '{0}'" @@ -277,7 +277,7 @@ discriminatorIncompatibleWithAllOfExceptionMessage = "The parameter 'Discriminator' is incompatible with 'allOf'." noNameForWebSocketSendMessageExceptionMessage = 'No Name for a WebSocket to send message to supplied.' hashtableMiddlewareNoLogicExceptionMessage = 'A Hashtable Middleware supplied has no Logic defined.' - openApiInfoMessage = 'OpenAPI Info:' + openApiInfoMessage = 'OpenAPI Information:' invalidSchemeForAuthValidatorExceptionMessage = "The supplied '{0}' Scheme for the '{1}' authentication validator requires a valid ScriptBlock." sseFailedToBroadcastExceptionMessage = 'SSE failed to broadcast due to defined SSE broadcast level for {0}: {1}' adModuleWindowsOnlyExceptionMessage = 'Active Directory module only available on Windows OS.' @@ -291,5 +291,35 @@ 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' + resumingMessage = 'Resuming' + serverControlCommandsTitle = 'Server Control Commands:' + gracefullyTerminateMessage = 'Gracefully terminate the server.' + restartServerMessage = 'Restart the server and reload configurations.' + resumeServerMessage = 'Resume the server.' + suspendServerMessage = 'Suspend the server.' + startingMessage = 'Starting' + restartingMessage = 'Restarting' + suspendedMessage = 'Suspended' + runningMessage = 'Running' + openHttpEndpointMessage = 'Open the default HTTP endpoint in the default browser.' + terminatedMessage = 'Terminated' + showMetricsMessage = 'Show Metrics' + clearConsoleMessage = 'Clear the Console' + serverMetricsMessage = 'Server Metrics' + totalUptimeMessage = 'Total Uptime:' + uptimeSinceLastRestartMessage = 'Uptime Since Last Restart:' + totalRestartMessage = 'Total Number of Restarts:' + defaultEndpointAlreadySetExceptionMessage = "A default endpoint for the type '{0}' is already set. Only one default endpoint is allowed per type." + enableHttpServerMessage = 'Enable HTTP Server' + disableHttpServerMessage = 'Disable HTTP Server' + showHelpMessage = 'Show Help' + hideHelpMessage = 'Hide Help' + hideEndpointsMessage = 'Hide Endpoints' + showEndpointsMessage = 'Show Endpoints' + hideOpenAPIMessage = 'Hide OpenAPI' + showOpenAPIMessage = 'Show OpenAPI' + enableQuietModeMessage = 'Enable Quiet Mode' + disableQuietModeMessage = 'Disable Quiet Mode' } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 9c2ee1194..9b7175821 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'No se encontró la ruta para cargar {0}: {1}' failedToImportModuleExceptionMessage = 'Error al importar el módulo: {0}' endpointNotExistExceptionMessage = "No existe un punto de conexión con el protocolo '{0}' y la dirección '{1}' o la dirección local '{2}'." - terminatingMessage = 'Terminando...' + terminatingMessage = 'Terminando' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'No se proporcionaron comandos para convertir a Rutas.' invalidTaskTypeExceptionMessage = 'El tipo de tarea no es válido, se esperaba [System.Threading.Tasks.Task] o [hashtable].' alreadyConnectedToWebSocketExceptionMessage = "Ya conectado al WebSocket con el nombre '{0}'" @@ -291,5 +291,35 @@ 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' + resumingMessage = 'Reanudando' + 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.' + startingMessage = 'Iniciando' + restartingMessage = 'Reiniciando' + suspendedMessage = 'Suspendido' + runningMessage = 'En ejecución' + openHttpEndpointMessage = 'Abrir el primer endpoint HTTP en el navegador predeterminado.' + terminatedMessage = 'Terminado' + showMetricsMessage = 'Mostrar métricas' + clearConsoleMessage = 'Limpiar la consola' + serverMetricsMessage = 'Métricas del servidor' + totalUptimeMessage = 'Tiempo total de actividad:' + uptimeSinceLastRestartMessage = 'Tiempo de actividad desde el último reinicio:' + totalRestartMessage = 'Número total de reinicios:' + defaultEndpointAlreadySetExceptionMessage = "Ya se ha establecido un punto final predeterminado para el tipo '{0}'. Solo se permite un punto final predeterminado por tipo." + enableHttpServerMessage = 'Habilitar servidor HTTP' + disableHttpServerMessage = 'Deshabilitar servidor HTTP' + showHelpMessage = 'Mostrar ayuda' + hideHelpMessage = 'Ocultar ayuda' + hideEndpointsMessage = 'Ocultar endpoints' + showEndpointsMessage = 'Mostrar endpoints' + hideOpenAPIMessage = 'Ocultar OpenAPI' + showOpenAPIMessage = 'Mostrar OpenAPI' + enableQuietModeMessage = 'Activar modo silencioso' + disableQuietModeMessage = 'Desactivar modo silencioso' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 2c9dfc579..f068b6f12 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'Chemin à charger {0} non trouvé : {1}' failedToImportModuleExceptionMessage = "Échec de l'importation du module : {0}" endpointNotExistExceptionMessage = "Un point de terminaison avec le protocole '{0}' et l'adresse '{1}' ou l'adresse locale '{2}' n'existe pas." - terminatingMessage = 'Terminaison...' + terminatingMessage = 'Terminaison' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Aucune commande fournie pour convertir en routes.' invalidTaskTypeExceptionMessage = "Le type de tâche n'est pas valide, attendu [System.Threading.Tasks.Task] ou [hashtable]." alreadyConnectedToWebSocketExceptionMessage = "Déjà connecté au WebSocket avec le nom '{0}'" @@ -291,5 +291,35 @@ 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' + resumingMessage = 'Reprise' + 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.' + startingMessage = 'Démarrage' + 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.' + terminatedMessage = 'Terminé' + showMetricsMessage = 'Afficher les métriques' + clearConsoleMessage = 'Effacer la console' + serverMetricsMessage = 'Métriques du serveur' + totalUptimeMessage = 'Temps de fonctionnement total :' + uptimeSinceLastRestartMessage = 'Temps de fonctionnement depuis le dernier redémarrage :' + totalRestartMessage = 'Nombre total de redémarrages :' + defaultEndpointAlreadySetExceptionMessage = "Un point de terminaison par défaut pour le type '{0}' est déjà défini. Un seul point de terminaison par défaut est autorisé par type." + enableHttpServerMessage = 'Activer le serveur HTTP' + disableHttpServerMessage = 'Désactiver le serveur HTTP' + showHelpMessage = "Afficher l'aide" + hideHelpMessage = "Masquer l'aide" + hideEndpointsMessage = 'Masquer les endpoints' + showEndpointsMessage = 'Afficher les endpoints' + hideOpenAPIMessage = 'Masquer OpenAPI' + showOpenAPIMessage = 'Afficher OpenAPI' + enableQuietModeMessage = 'Activer le mode silencieux' + disableQuietModeMessage = 'Désactiver le mode silencieux' } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 999bf85c3..d4f0517c0 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'Percorso per caricare {0} non trovato: {1}' failedToImportModuleExceptionMessage = 'Importazione del modulo non riuscita: {0}' endpointNotExistExceptionMessage = "'Endpoint' con protocollo '{0}' e indirizzo '{1}' o indirizzo locale '{2}' non esiste." - terminatingMessage = 'Terminazione...' + terminatingMessage = 'Terminazione' noCommandsSuppliedToConvertToRoutesExceptionMessage = "Nessun comando fornito per convertirlo in 'route'." invalidTaskTypeExceptionMessage = 'Il tipo di attività non è valido, previsto [System.Threading.Tasks.Task] o [hashtable].' alreadyConnectedToWebSocketExceptionMessage = "Già connesso al WebSocket con il nome '{0}'" @@ -291,5 +291,35 @@ 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' + resumingMessage = 'Ripresa' + 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.' + startingMessage = 'Avvio' + restartingMessage = 'Riavvio' + suspendedMessage = 'Sospeso' + runningMessage = 'In esecuzione' + openHttpEndpointMessage = 'Apri il predefinito endpoint HTTP nel browser predefinito.' + terminatedMessage = 'Terminato' + showMetricsMessage = 'Mostra metriche' + clearConsoleMessage = 'Cancella la console' + serverMetricsMessage = 'Metriche del server' + totalUptimeMessage = 'Tempo totale di attività:' + uptimeSinceLastRestartMessage = "Tempo di attività dall'ultimo riavvio:" + totalRestartMessage = 'Numero totale di riavvii:' + defaultEndpointAlreadySetExceptionMessage = "Un endpoint predefinito per il tipo '{0}' è già impostato. È consentito un solo endpoint predefinito per tipo." + enableHttpServerMessage = 'Abilita il server HTTP' + disableHttpServerMessage = 'Disabilita il server HTTP' + showHelpMessage = 'Mostra aiuto' + hideHelpMessage = 'Nascondi aiuto' + hideEndpointsMessage = 'Nascondi gli endpoint' + showEndpointsMessage = 'Mostra gli endpoint' + hideOpenAPIMessage = 'Nascondi OpenAPI' + showOpenAPIMessage = 'Mostra OpenAPI' + enableQuietModeMessage = 'Abilita la modalità silenziosa' + disableQuietModeMessage = 'Disabilita la modalità silenziosa' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index e65627c59..dee48a022 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = '読み込むパス{0}が見つかりません: {1}' failedToImportModuleExceptionMessage = 'モジュールのインポートに失敗しました: {0}' endpointNotExistExceptionMessage = "プロトコル'{0}'、アドレス'{1}'またはローカルアドレス'{2}'のエンドポイントが存在しません。" - terminatingMessage = '終了中...' + terminatingMessage = '終了中' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'ルートに変換するためのコマンドが提供されていません。' invalidTaskTypeExceptionMessage = 'タスクタイプが無効です。予期されるタイプ:[System.Threading.Tasks.Task]または[hashtable]' alreadyConnectedToWebSocketExceptionMessage = "名前 '{0}' の WebSocket に既に接続されています" @@ -291,5 +291,35 @@ 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 = 'サーバーを一時停止します。' + startingMessage = '開始中' + restartingMessage = '再起動中' + suspendedMessage = '一時停止中' + runningMessage = '実行中' + openHttpEndpointMessage = 'デフォルトのブラウザで最初の HTTP エンドポイントを開きます。' + terminatedMessage = '終了しました' + showMetricsMessage = 'メトリクスを表示' + clearConsoleMessage = 'コンソールをクリア' + serverMetricsMessage = 'サーバーメトリクス' + totalUptimeMessage = '総稼働時間:' + uptimeSinceLastRestartMessage = '最後の再起動からの稼働時間:' + totalRestartMessage = '再起動の総数:' + defaultEndpointAlreadySetExceptionMessage = "タイプ '{0}' のデフォルトエンドポイントは既に設定されています。タイプごとに1つのデフォルトエンドポイントのみ許可されています。" + enableHttpServerMessage = 'HTTPサーバーを有効化する' + disableHttpServerMessage = 'HTTPサーバーを無効化する' + showHelpMessage = 'ヘルプを表示' + hideHelpMessage = 'ヘルプを非表示' + hideEndpointsMessage = 'エンドポイントを非表示' + showEndpointsMessage = 'エンドポイントを表示' + hideOpenAPIMessage = 'OpenAPIを非表示' + showOpenAPIMessage = 'OpenAPIを表示' + enableQuietModeMessage = 'クワイエットモードを有効化' + disableQuietModeMessage = 'クワイエットモードを無効化' } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index f64f0c61f..95e60140e 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = '로드할 경로 {0}을(를) 찾을 수 없습니다: {1}' failedToImportModuleExceptionMessage = '모듈을 가져오지 못했습니다: {0}' endpointNotExistExceptionMessage = "프로토콜 '{0}' 및 주소 '{1}' 또는 로컬 주소 '{2}'가 있는 엔드포인트가 존재하지 않습니다." - terminatingMessage = '종료 중...' + terminatingMessage = '종료 중' noCommandsSuppliedToConvertToRoutesExceptionMessage = '경로로 변환할 명령이 제공되지 않았습니다.' invalidTaskTypeExceptionMessage = '작업 유형이 유효하지 않습니다. 예상된 유형: [System.Threading.Tasks.Task] 또는 [hashtable]' alreadyConnectedToWebSocketExceptionMessage = "이름이 '{0}'인 WebSocket에 이미 연결되어 있습니다." @@ -291,5 +291,35 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' 작업은 요청 본문을 가질 수 없습니다. 이 제한을 무시하려면 -AllowNonStandardBody를 사용하세요." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' - LocalEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." + localEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." + suspendingMessage = '중단' + resumingMessage = '재개' + serverControlCommandsTitle = '서버 제어 명령:' + gracefullyTerminateMessage = '서버를 정상적으로 종료합니다.' + restartServerMessage = '서버를 재시작하고 설정을 다시 로드합니다.' + resumeServerMessage = '서버를 재개합니다.' + suspendServerMessage = '서버를 일시 중지합니다.' + startingMessage = '시작 중' + restartingMessage = '재시작 중' + suspendedMessage = '일시 중지됨' + runningMessage = '실행 중' + openHttpEndpointMessage = '기본 브라우저에서 첫 번째 HTTP 엔드포인트를 엽니다.' + terminatedMessage = '종료됨' + showMetricsMessage = '메트릭 표시' + clearConsoleMessage = '콘솔 지우기' + serverMetricsMessage = '서버 메트릭' + totalUptimeMessage = '총 가동 시간:' + uptimeSinceLastRestartMessage = '마지막 재시작 이후 가동 시간:' + totalRestartMessage = '총 재시작 횟수:' + defaultEndpointAlreadySetExceptionMessage = "'{0}' 유형에 대한 기본 엔드포인트가 이미 설정되어 있습니다. 유형당 하나의 기본 엔드포인트만 허용됩니다." + enableHttpServerMessage = 'HTTP 서버 활성화' + disableHttpServerMessage = 'HTTP 서버 비활성화' + showHelpMessage = '도움말 표시' + hideHelpMessage = '도움말 숨기기' + hideEndpointsMessage = '엔드포인트 숨기기' + showEndpointsMessage = '엔드포인트 표시' + hideOpenAPIMessage = 'OpenAPI 숨기기' + showOpenAPIMessage = 'OpenAPI 표시' + enableQuietModeMessage = '조용한 모드 활성화' + disableQuietModeMessage = '조용한 모드 비활성화' } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index d7933a0d9..f254feb64 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -28,7 +28,7 @@ pathToLoadNotFoundExceptionMessage = 'Pad om te laden {0} niet gevonden: {1}' failedToImportModuleExceptionMessage = 'Kon module niet importeren: {0}' endpointNotExistExceptionMessage = "Eindpunt met protocol '{0}' en adres '{1}' of lokaal adres '{2}' bestaat niet." - terminatingMessage = 'Beëindigen...' + terminatingMessage = 'Beëindigen' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Geen opdrachten opgegeven om om te zetten naar routes.' invalidTaskTypeExceptionMessage = 'Taaktype is ongeldig, verwacht ofwel [System.Threading.Tasks.Task] of [hashtable]' alreadyConnectedToWebSocketExceptionMessage = "Al verbonden met WebSocket met naam '{0}'" @@ -291,5 +291,35 @@ 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 = 'Onderbreken' + resumingMessage = 'Hervatten' + serverControlCommandsTitle = "Serverbedieningscommando's:" + gracefullyTerminateMessage = 'Server netjes afsluiten.' + restartServerMessage = 'Server opnieuw starten en configuraties herladen.' + resumeServerMessage = 'Server hervatten.' + suspendServerMessage = 'Server pauzeren.' + startingMessage = 'Starten' + restartingMessage = 'Herstarten' + suspendedMessage = 'Gepauzeerd' + runningMessage = 'Actief' + openHttpEndpointMessage = 'Open het eerste HTTP-eindpunt in de standaardbrowser.' + terminatedMessage = 'Beëindigd' + showMetricsMessage = 'Toon statistieken' + clearConsoleMessage = 'Console wissen' + serverMetricsMessage = 'Serverstatistieken' + totalUptimeMessage = 'Totale uptime:' + uptimeSinceLastRestartMessage = 'Uptime sinds laatste herstart:' + totalRestartMessage = 'Totaal aantal herstarts:' + defaultEndpointAlreadySetExceptionMessage = "Er is al een standaardendpoint ingesteld voor het type '{0}'. Er is slechts één standaardendpoint per type toegestaan." + enableHttpServerMessage = 'HTTP-server inschakelen' + disableHttpServerMessage = 'HTTP-server uitschakelen' + showHelpMessage = 'Hulp weergeven' + hideHelpMessage = 'Hulp verbergen' + hideEndpointsMessage = 'Endpoints verbergen' + showEndpointsMessage = 'Endpoints weergeven' + hideOpenAPIMessage = 'OpenAPI verbergen' + showOpenAPIMessage = 'OpenAPI weergeven' + enableQuietModeMessage = 'Stille modus inschakelen' + disableQuietModeMessage = 'Stille modus uitschakelen' } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index cd632c469..7319ae592 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'Ścieżka do załadowania {0} nie znaleziona: {1}' failedToImportModuleExceptionMessage = 'Nie udało się zaimportować modułu: {0}' endpointNotExistExceptionMessage = "Punkt końcowy z protokołem '{0}' i adresem '{1}' lub adresem lokalnym '{2}' nie istnieje." - terminatingMessage = 'Kończenie...' + terminatingMessage = 'Kończenie' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Nie dostarczono żadnych poleceń do konwersji na trasy.' invalidTaskTypeExceptionMessage = 'Typ zadania jest nieprawidłowy, oczekiwano [System.Threading.Tasks.Task] lub [hashtable]' alreadyConnectedToWebSocketExceptionMessage = "Już połączono z WebSocket o nazwie '{0}'" @@ -291,5 +291,35 @@ 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' + resumingMessage = 'Wznawianie' + 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.' + startingMessage = 'Rozpoczynanie' + restartingMessage = 'Ponowne uruchamianie' + suspendedMessage = 'Wstrzymany' + runningMessage = 'Działa' + openHttpEndpointMessage = 'Otwórz pierwszy punkt końcowy HTTP w domyślnej przeglądarce.' + terminatedMessage = 'Zakończono' + showMetricsMessage = 'Pokaż metryki' + clearConsoleMessage = 'Wyczyść konsolę' + serverMetricsMessage = 'Metryki serwera' + totalUptimeMessage = 'Całkowity czas działania:' + uptimeSinceLastRestartMessage = 'Czas działania od ostatniego restartu:' + totalRestartMessage = 'Całkowita liczba restartów:' + defaultEndpointAlreadySetExceptionMessage = "Domyślny punkt końcowy dla typu '{0}' został już ustawiony. Dopuszczalny jest tylko jeden domyślny punkt końcowy na typ." + enableHttpServerMessage = 'Włącz serwer HTTP' + disableHttpServerMessage = 'Wyłącz serwer HTTP' + showHelpMessage = 'Pokaż pomoc' + hideHelpMessage = 'Ukryj pomoc' + hideEndpointsMessage = 'Ukryj punkty końcowe' + showEndpointsMessage = 'Pokaż punkty końcowe' + hideOpenAPIMessage = 'Ukryj OpenAPI' + showOpenAPIMessage = 'Pokaż OpenAPI' + enableQuietModeMessage = 'Włącz tryb cichy' + disableQuietModeMessage = 'Wyłącz tryb cichy' } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index a0604c179..4e1a8c613 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = 'Caminho para carregar {0} não encontrado: {1}' failedToImportModuleExceptionMessage = 'Falha ao importar módulo: {0}' endpointNotExistExceptionMessage = "O ponto de extremidade com o protocolo '{0}' e endereço '{1}' ou endereço local '{2}' não existe." - terminatingMessage = 'Terminando...' + terminatingMessage = 'Terminando' noCommandsSuppliedToConvertToRoutesExceptionMessage = 'Nenhum comando fornecido para converter em Rotas.' invalidTaskTypeExceptionMessage = 'O tipo de tarefa é inválido, esperado [System.Threading.Tasks.Task] ou [hashtable].' alreadyConnectedToWebSocketExceptionMessage = "Já conectado ao websocket com o nome '{0}'" @@ -291,5 +291,35 @@ 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 = 'Suspensão' + resumingMessage = 'Retomada' + 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.' + startingMessage = 'Iniciando' + restartingMessage = 'Reiniciando' + suspendedMessage = 'Suspenso' + runningMessage = 'Executando' + openHttpEndpointMessage = 'Abrir o primeiro endpoint HTTP no navegador padrão.' + terminatedMessage = 'Terminado' + showMetricsMessage = 'Mostrar métricas' + clearConsoleMessage = 'Limpar o console' + serverMetricsMessage = 'Métricas do servidor' + totalUptimeMessage = 'Tempo total de atividade:' + uptimeSinceLastRestartMessage = 'Tempo de atividade desde o último reinício:' + totalRestartMessage = 'Número total de reinicializações:' + defaultEndpointAlreadySetExceptionMessage = "Um endpoint padrão para o tipo '{0}' já está definido. Apenas um endpoint padrão é permitido por tipo." + enableHttpServerMessage = 'Ativar servidor HTTP' + disableHttpServerMessage = 'Desativar servidor HTTP' + showHelpMessage = 'Mostrar ajuda' + hideHelpMessage = 'Ocultar ajuda' + hideEndpointsMessage = 'Ocultar endpoints' + showEndpointsMessage = 'Mostrar endpoints' + hideOpenAPIMessage = 'Ocultar OpenAPI' + showOpenAPIMessage = 'Mostrar OpenAPI' + enableQuietModeMessage = 'Ativar modo silencioso' + disableQuietModeMessage = 'Desativar modo silencioso' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 26b013c95..8ac1c86b9 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -27,7 +27,7 @@ pathToLoadNotFoundExceptionMessage = '未找到要加载的路径 {0}: {1}' failedToImportModuleExceptionMessage = '导入模块失败: {0}' endpointNotExistExceptionMessage = "具有协议 '{0}' 和地址 '{1}' 或本地地址 '{2}' 的端点不存在。" - terminatingMessage = '正在终止...' + terminatingMessage = '正在终止' noCommandsSuppliedToConvertToRoutesExceptionMessage = '未提供要转换为路由的命令。' invalidTaskTypeExceptionMessage = '任务类型无效,预期类型为[System.Threading.Tasks.Task]或[hashtable]。' alreadyConnectedToWebSocketExceptionMessage = "已连接到名为 '{0}' 的 WebSocket" @@ -291,5 +291,35 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' 操作无法包含请求体。使用 -AllowNonStandardBody 以解除此限制。" fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' - LocalEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" + localEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" + suspendingMessage = '暂停' + resumingMessage = '恢复' + serverControlCommandsTitle = '服务器控制命令:' + gracefullyTerminateMessage = '正常终止服务器。' + restartServerMessage = '重启服务器并重新加载配置。' + resumeServerMessage = '恢复服务器。' + suspendServerMessage = '暂停服务器。' + startingMessage = '启动中' + restartingMessage = '正在重启' + suspendedMessage = '已暂停' + runningMessage = '运行中' + openHttpEndpointMessage = '在默认浏览器中打开第一个 HTTP 端点。' + terminatedMessage = '已终止' + showMetricsMessage = '显示指标' + clearConsoleMessage = '清除控制台' + serverMetricsMessage = '服务器指标' + totalUptimeMessage = '总运行时间:' + uptimeSinceLastRestartMessage = '自上次重启后的运行时间:' + totalRestartMessage = '重启总次数:' + defaultEndpointAlreadySetExceptionMessage = "类型 '{0}' 的默认端点已设置。每种类型只允许一个默认端点。" + enableHttpServerMessage = '启用HTTP服务器' + disableHttpServerMessage = '禁用HTTP服务器' + showHelpMessage = '显示帮助' + hideHelpMessage = '隐藏帮助' + hideEndpointsMessage = '隐藏端点' + showEndpointsMessage = '显示端点' + hideOpenAPIMessage = '隐藏OpenAPI' + showOpenAPIMessage = '显示OpenAPI' + enableQuietModeMessage = '启用安静模式' + disableQuietModeMessage = '禁用安静模式' } \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index ad02ac21c..f8929ec77 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -144,6 +144,7 @@ 'Get-PodeCurrentRunspaceName', 'Set-PodeCurrentRunspaceName', 'Invoke-PodeGC', + 'Start-PodeSleep', # routes 'Add-PodeRoute', @@ -227,6 +228,7 @@ 'New-PodeMiddleware', 'Add-PodeBodyParser', 'Remove-PodeBodyParser', + 'Test-PodeMiddleware', # sessions 'Enable-PodeSessionMiddleware', @@ -307,6 +309,12 @@ 'Get-PodeServerDefaultSecret', 'Wait-PodeDebugger', 'Get-PodeVersion', + 'Suspend-PodeServer', + 'Resume-PodeServer', + 'Get-PodeServerState', + 'Test-PodeServerState', + 'Enable-PodeServer', + 'Disable-PodeServer', # openapi 'Enable-PodeOpenApi', @@ -319,6 +327,7 @@ 'Test-PodeOADefinitionTag', 'Test-PodeOADefinition', 'Rename-PodeOADefinitionTag', + 'Test-PodeOAEnabled', # properties 'New-PodeOAIntProperty', 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/CancellationToken.ps1 b/src/Private/CancellationToken.ps1 new file mode 100644 index 000000000..13fbd140d --- /dev/null +++ b/src/Private/CancellationToken.ps1 @@ -0,0 +1,473 @@ + +<# +.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. + +.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 'Suspend' type + Reset-PodeCancellationToken -Type Suspend + +.NOTES + This function is used to manage cancellation tokens in Pode's internal context. +#> +function Reset-PodeCancellationToken { + param( + [Parameter(Mandatory = $true)] + [validateset( 'Cancellation' , 'Restart', 'Suspend', 'Resume', 'Terminate', 'Start', 'Disable' )] + [string[]] + $Type + ) + foreach ($item in $type) { + # Ensure cleanup of disposable tokens + Close-PodeDisposable -Disposable $PodeContext.Tokens[$item] + + # Reinitialize the Token + $PodeContext.Tokens[$item] = [System.Threading.CancellationTokenSource]::new() + } +} + +<# +.SYNOPSIS + Closes and disposes of specified cancellation tokens in the Pode context. + +.DESCRIPTION + The `Close-PodeCancellationToken` function ensures proper cleanup of disposable cancellation tokens + within the `$PodeContext`. It allows you to specify one or more token types to close and dispose of, + or you can dispose of all tokens if no type is specified. + + Supported token types include: + - `Cancellation` + - `Restart` + - `Suspend` + - `Resume` + - `Terminate` + - `Start` + - `Disable` + + This function is essential for managing resources during the lifecycle of a Pode application, + especially when cleaning up during shutdown or restarting. + +.PARAMETER Type + Specifies the type(s) of cancellation tokens to close. Valid values are: + `Cancellation`, `Restart`, `Suspend`, `Resume`, `Terminate`, `Start`,'Disable'. + + If this parameter is not specified, all tokens in `$PodeContext.Tokens` will be disposed of. + +.EXAMPLE + Close-PodeCancellationToken -Type 'Suspend' + Closes and disposes of the `Suspend` cancellation token in the Pode context. + +.EXAMPLE + Close-PodeCancellationToken -Type 'Restart', 'Terminate' + Closes and disposes of the `Restart` and `Terminate` cancellation tokens in the Pode context. + +.EXAMPLE + Close-PodeCancellationToken + Closes and disposes of all tokens in the Pode context. + +.NOTES + This is an internal function and may change in future releases of Pode. + +#> + + +function Close-PodeCancellationToken { + [CmdletBinding()] + param( + [Parameter()] + [ValidateSet('Cancellation', 'Restart', 'Suspend', 'Resume', 'Terminate', 'Start', 'Disable' )] + [string[]] + $Type + ) + if ($null -eq $Type) { + $PodeContext.Tokens.Values | Close-PodeDisposable + } + else { + foreach ($tokenType in $Type) { + # Ensure cleanup of disposable tokens + Close-PodeDisposable -Disposable $PodeContext.Tokens[$tokenType] + } + } +} + + + + +<# +.SYNOPSIS + Waits for Pode suspension cancellation token to be reset. + +.DESCRIPTION + The `Test-PodeSuspensionToken` function checks the status of the suspension cancellation token within the `$PodeContext`. + It enters a loop to wait for the `Suspend` cancellation token to be reset before proceeding. + Each loop iteration includes a 1-second delay to minimize resource usage. + The function returns a boolean indicating whether the suspension token was initially requested. + +.EXAMPLE + Test-PodeSuspensionToken + Waits for the suspension token to be reset in the Pode context. + +.OUTPUTS + [bool] + Indicates whether the suspension token was initially requested. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeSuspensionToken { + # Check if the Suspend token was initially requested + $suspended = $PodeContext.Tokens.Suspend.IsCancellationRequested + + # Wait for the Suspend token to be reset + while ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Start-Sleep -Seconds 1 + } + + # Return whether the suspension token was initially requested + return $suspended +} + +<# +.SYNOPSIS + Creates a set of cancellation tokens for managing Pode application states. + +.DESCRIPTION + The `Initialize-PodeCancellationToken` function initializes and returns a hashtable containing + multiple cancellation tokens used for managing various states in a Pode application. + These tokens provide coordinated control over application operations, such as cancellation, + restart, suspension, resumption, termination, and start operations. + + The returned hashtable includes the following keys: + - `Cancellation`: A token specifically for managing endpoint cancellation tasks. + - `Restart`: A token for managing application restarts. + - `Suspend`: A token for handling suspension operations. + - `Resume`: A token for resuming operations after suspension. + - `Terminate`: A token for managing application termination. + - `Start`: A token for monitoring application startup. + - `Disable`: A token for denying web access. + +.EXAMPLE + $tokens = Initialize-PodeCancellationToken + Initializes a set of cancellation tokens and stores them in the `$tokens` variable. + +.OUTPUTS + [hashtable] + A hashtable containing initialized cancellation tokens. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Initialize-PodeCancellationToken { + # Initialize and return a hashtable containing various cancellation tokens. + return @{ + # A cancellation token specifically for managing endpoint cancellation tasks. + Cancellation = [System.Threading.CancellationTokenSource]::new() + + # A cancellation token specifically for managing application restart operations. + Restart = [System.Threading.CancellationTokenSource]::new() + + # A cancellation token for suspending operations in the Pode application. + Suspend = [System.Threading.CancellationTokenSource]::new() + + # A cancellation token for resuming operations after a suspension. + Resume = [System.Threading.CancellationTokenSource]::new() + + # A cancellation token for managing application termination. + Terminate = [System.Threading.CancellationTokenSource]::new() + + # A cancellation token for monitoring application startup. + Start = [System.Threading.CancellationTokenSource]::new() + + # A cancellation token for denying any web request. + Disable = [System.Threading.CancellationTokenSource]::new() + } +} + + + +<# +.SYNOPSIS + Sets the Resume token for the Pode server to resume its operation from a suspended state. + +.DESCRIPTION + The Set-PodeResumeToken function ensures that the Resume token's cancellation is requested to signal that the server should + resume its operation. Additionally, it resets other related tokens, such as Cancellation and Suspend, if they are in a requested state. + This function prevents conflicts between tokens and ensures proper state management in the Pode server. + +.NOTES + This is an internal function and may change in future releases of Pode. + +.EXAMPLE + Set-PodeResumeToken + + Signals the Pode server to resume operations and resets relevant tokens. +#> +function Set-PodeResumeToken { + + # Ensure the Resume token is in a cancellation requested state + Close-PodeCancellationTokenRequest -Type Resume + + # If the Cancellation token is in a requested state, reset it (unexpected scenario) + if ($PodeContext.Tokens.Cancellation.IsCancellationRequested) { + Reset-PodeCancellationToken -Type Cancellation + } + + # Reset the Suspend token if it is in a cancellation requested state + if ($PodeContext.Tokens.Suspend.IsCancellationRequested) { + Reset-PodeCancellationToken -Type Suspend + } +} + + +<# +.SYNOPSIS + Sets the Suspend token for the Pode server to transition into a suspended state. + +.DESCRIPTION + The Set-PodeSuspendToken function ensures that the Suspend token's cancellation is requested to signal that the server should + transition into a suspended state. Additionally, it sets the Cancellation token to prevent further operations while the server + is suspended. + +.NOTES + This is an internal function and may change in future releases of Pode. + +.EXAMPLE + Set-PodeSuspendToken + + Signals the Pode server to transition into a suspended state by setting the Suspend token and the Cancellation token. +#> +function Set-PodeSuspendToken { + # Ensure the Suspend and Cancellation tokens is in a cancellation requested state + Close-PodeCancellationTokenRequest -Type Suspend, Cancellation +} + + +<# +.SYNOPSIS + Sets the cancellation token(s) for the specified Pode server actions. + +.DESCRIPTION + The `Close-PodeCancellationTokenRequest` function cancels one or more specified tokens within the Pode server. + These tokens are used to manage the server's lifecycle actions, such as Restart, Suspend, Resume, or Terminate. + The function takes a mandatory parameter `$Type`, which determines the token(s) to be canceled. + Supported types include: `Cancellation`, `Restart`, `Suspend`, `Resume`, `Terminate`, `Start`, and `Disable`. + +.PARAMETER Type + Specifies the token(s) to be canceled. This parameter accepts one or more values from a predefined set. + Allowed values: `Cancellation`, `Restart`, `Suspend`, `Resume`, `Terminate`, `Start`, `Disable`. + +.EXAMPLE + Close-PodeCancellationTokenRequest -Type 'Restart' + + Cancels the Restart token for the Pode server. + +.EXAMPLE + Close-PodeCancellationTokenRequest -Type 'Suspend','Terminate' + + Cancels both the Suspend and Terminate tokens for the Pode server. + +.NOTES + This function is an internal utility and may change in future releases of Pode. +#> +function Close-PodeCancellationTokenRequest { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Cancellation', 'Restart', 'Suspend', 'Resume', 'Terminate', 'Start', 'Disable')] + [string[]] + $Type + ) + + # Iterate over each provided type and cancel its corresponding token if not already canceled + foreach ($item in $Type) { + if ($PodeContext.Tokens.ContainsKey($item)) { + if (! $PodeContext.Tokens[$item].IsCancellationRequested) { + # Cancel the specified token + $PodeContext.Tokens[$item].Cancel() + } + } + } +} + +<# +.SYNOPSIS + Waits for a specific Pode server cancellation token to be reset. + +.DESCRIPTION + The `Wait-PodeCancellationTokenRequest` function continuously checks the status of a specified cancellation token + in the Pode server context. It pauses execution in a loop until the token's cancellation request is cleared. + +.PARAMETER Type + Specifies the token to wait for. This parameter accepts one value from a predefined set. + Allowed values: `Cancellation`, `Restart`, `Suspend`, `Resume`, `Terminate`, `Start`, `Disable`. + +.EXAMPLE + Wait-PodeCancellationTokenRequest -Type 'Restart' + + Waits until the Restart token is reset and no longer has a cancellation request. + +.EXAMPLE + Wait-PodeCancellationTokenRequest -Type 'Suspend' + + Waits for the Suspend token to be reset, pausing execution until the token is no longer in a cancellation state. + +.NOTES + - This function is part of Pode's internal utilities and may change in future releases. + - It uses a simple loop with a 1-second sleep interval to reduce CPU usage while waiting. + +#> +function Wait-PodeCancellationTokenRequest { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Cancellation', 'Restart', 'Suspend', 'Resume', 'Terminate', 'Start', 'Disable')] + [string] + $Type + ) + + # Wait for the token to be reset, with exponential back-off + $count = 1 + while ($PodeContext.Tokens[$Type].IsCancellationRequested) { + Start-Sleep -Milliseconds (100 * $count) + $count = [System.Math]::Min($count + 1, 20) + } +} + +<# +.SYNOPSIS + Evaluates whether a specified Pode server token has an active cancellation request. + +.DESCRIPTION + The `Test-PodeCancellationTokenRequest` function checks the cancellation state of a given token + in the Pode server context. It determines whether the token has been marked for cancellation + and optionally waits for the cancellation to occur if the `-Wait` parameter is specified. + +.PARAMETER Type + Specifies the token to check for an active cancellation request. + Acceptable values include predefined token types in Pode: + - `Cancellation` + - `Restart` + - `Suspend` + - `Resume` + - `Terminate` + - `Start` + - `Disable` + +.PARAMETER Wait + If specified, waits until the token's cancellation request becomes active before returning the result. + +.OUTPUTS + [bool] Returns `$true` if the specified token has an active cancellation request, otherwise `$false`. + +.EXAMPLE + Test-PodeCancellationTokenRequest -Type 'Restart' + + Checks if the Restart token has an active cancellation request and returns `$true` or `$false`. + +.EXAMPLE + Test-PodeCancellationTokenRequest -Type 'Suspend' -Wait + + Waits until the Suspend token has an active cancellation request before returning `$true` or `$false`. + +.NOTES + This function is an internal utility for Pode and may be subject to change in future releases. +#> +function Test-PodeCancellationTokenRequest { + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Cancellation', 'Restart', 'Suspend', 'Resume', 'Terminate', 'Start', 'Disable')] + [string] + $Type, + + [switch] + $Wait + ) + + # Check if the specified token has an active cancellation request + $cancelled = $PodeContext.Tokens[$Type].IsCancellationRequested + + # If -Wait is specified, block until the token's cancellation request becomes active + if ($Wait) { + Wait-PodeCancellationTokenRequest -Type $Type + } + + return $cancelled +} + + +<# +.SYNOPSIS + Resolves cancellation token requests and executes corresponding server actions. + +.DESCRIPTION + This internal function evaluates cancellation token requests to handle actions + such as restarting the server, enabling/disabling the server, or suspending/resuming + its operations. It interacts with the Pode server's context and state to perform + the necessary operations based on the allowed actions and current state. + +.PARAMETER serverState + The current state of the Pode server, retrieved using Get-PodeServerState, + which determines whether actions like suspend, disable, or restart can be executed. + +.NOTES + This is an internal function and may change in future releases of Pode. + +.EXAMPLE + Resolve-PodeCancellationToken + Evaluates any pending cancellation token requests and applies the appropriate server actions. +#> + +function Resolve-PodeCancellationToken { + param( + [Parameter(Mandatory = $true)] + [Pode.PodeServerState] + $ServerState + ) + + if ($PodeContext.Server.AllowedActions.Restart -and (Test-PodeCancellationTokenRequest -Type Restart)) { + Restart-PodeInternalServer + return + } + + # Handle enable/disable server actions + if ($PodeContext.Server.AllowedActions.Disable -and ($ServerState -eq [Pode.PodeServerState]::Running)) { + if (Test-PodeServerIsEnabled) { + if (Test-PodeCancellationTokenRequest -Type Disable) { + Disable-PodeServerInternal + Show-PodeConsoleInfo -ShowTopSeparator + return + } + } + else { + if (! (Test-PodeCancellationTokenRequest -Type Disable)) { + Enable-PodeServerInternal + Show-PodeConsoleInfo -ShowTopSeparator + return + } + } + } + # Handle suspend/resume actions + if ($PodeContext.Server.AllowedActions.Suspend) { + if ((Test-PodeCancellationTokenRequest -Type Resume) -and ($ServerState -eq [Pode.PodeServerState]::Suspended)) { + Resume-PodeServerInternal -Timeout $PodeContext.Server.AllowedActions.Timeout.Resume + return + } + elseif ((Test-PodeCancellationTokenRequest -Type Suspend) -and ($ServerState -eq [Pode.PodeServerState]::Running)) { + Suspend-PodeServerInternal -Timeout $PodeContext.Server.AllowedActions.Timeout.Suspend + return + } + } +} diff --git a/src/Private/Console.ps1 b/src/Private/Console.ps1 new file mode 100644 index 000000000..dd50728fe --- /dev/null +++ b/src/Private/Console.ps1 @@ -0,0 +1,1453 @@ + +<# +.SYNOPSIS + Displays key information about the Pode server on the console. + +.DESCRIPTION + The Show-PodeConsoleInfo function provides detailed information about the current Pode server instance, + including version, process ID (PID), server state, active endpoints, and OpenAPI definitions. + The function supports clearing the console before displaying the details and can conditionally show additional + server control commands depending on the server state and configuration. + +.PARAMETER ClearHost + Clears the console screen before displaying server information. + +.PARAMETER Force + Overrides the console's quiet mode to display the server information. + +.PARAMETER ShowTopSeparator + Adds a horizontal divider line at the top of the console output. + +.NOTES + This is an internal function and may change in future releases of Pode. + It is intended for displaying real-time server information during runtime. +#> +function Show-PodeConsoleInfo { + param( + [switch] + $ClearHost, + + [switch] + $Force, + + [switch] + $ShowTopSeparator + ) + + # Exit the function if PodeContext is not initialized + # or if the console is in quiet mode and the Force switch is not used + if (!$PodeContext -or ($PodeContext.Server.Console.Quiet -and !$Force)) { + return + } + + # Retrieve the current server state and optionally include a timestamp. + $serverState = Get-PodeServerState + + # Determine status and additional display options based on the server state. + if ($serverState -eq [Pode.PodeServerState]::Suspended) { + $status = $Podelocale.suspendedMessage + $statusColor = [System.ConsoleColor]::Yellow + $showHelp = (!$PodeContext.Server.Console.DisableConsoleInput -and $PodeContext.Server.Console.ShowHelp) + $noHeaderNewLine = $false + $ctrlH = !$showHelp + $footerSeparator = $false + $topSeparator = $ShowTopSeparator.IsPresent + $headerSeparator = $true + } + elseif ($serverState -eq [Pode.PodeServerState]::Suspending) { + $status = $Podelocale.suspendingMessage + $statusColor = [System.ConsoleColor]::Yellow + $showHelp = $false + $noHeaderNewLine = $false + $ctrlH = $false + $footerSeparator = $false + $topSeparator = $false + $headerSeparator = $false + } + elseif ($serverState -eq [Pode.PodeServerState]::Resuming) { + $status = $Podelocale.resumingMessage + $statusColor = [System.ConsoleColor]::Yellow + $showHelp = $false + $noHeaderNewLine = $false + $ctrlH = $false + $footerSeparator = $false + $topSeparator = $false + $headerSeparator = $false + } + elseif ($serverState -eq [Pode.PodeServerState]::Restarting) { + $status = $Podelocale.restartingMessage + $statusColor = [System.ConsoleColor]::Yellow + $showHelp = $false + $noHeaderNewLine = $false + $ctrlH = $false + $footerSeparator = $false + $topSeparator = $false + $headerSeparator = $false + } + elseif ($serverState -eq [Pode.PodeServerState]::Starting) { + $status = $Podelocale.startingMessage + $statusColor = [System.ConsoleColor]::Yellow + $showHelp = $false + $noHeaderNewLine = $false + $ctrlH = $false + $footerSeparator = $false + $topSeparator = $ShowTopSeparator.IsPresent + $headerSeparator = $false + } + elseif ($serverState -eq [Pode.PodeServerState]::Running) { + $status = $Podelocale.runningMessage + $statusColor = [System.ConsoleColor]::Green + $showHelp = (!$PodeContext.Server.Console.DisableConsoleInput -and $PodeContext.Server.Console.ShowHelp) + $noHeaderNewLine = $false + $ctrlH = !$showHelp + $footerSeparator = $false + $topSeparator = $ShowTopSeparator.IsPresent + $headerSeparator = $true + } + elseif ($serverState -eq [Pode.PodeServerState]::Terminating) { + $status = $Podelocale.terminatingMessage + $statusColor = [System.ConsoleColor]::Red + $showHelp = $false + $noHeaderNewLine = $false + $ctrlH = $false + $footerSeparator = $false + $topSeparator = $false + $headerSeparator = $false + } + elseif ($serverState -eq [Pode.PodeServerState]::Terminated) { + $status = $Podelocale.terminatedMessage + $statusColor = [System.ConsoleColor]::Red + $showHelp = $false + $noHeaderNewLine = $false + $ctrlH = $false + $footerSeparator = $false + $topSeparator = $ShowTopSeparator.IsPresent + $headerSeparator = $true + } + + if ($ClearHost -or $PodeContext.Server.Console.ClearHost) { + Clear-Host + } + elseif ($topSeparator ) { + # Write a horizontal divider line to the console. + Write-PodeHostDivider -Force $true + } + + # Write the header line with dynamic status color + Write-PodeConsoleHeader -Status $Status -StatusColor $StatusColor -Force:$Force -NoNewLine:$noHeaderNewLine + + # Optionally display a horizontal divider after the header. + if ($headerSeparator) { + # Write a horizontal divider line to the console. + Write-PodeHostDivider -Force $true + } + + # Display endpoints and OpenAPI information if the server is running. + if ($serverState -eq [Pode.PodeServerState]::Running) { + if ($PodeContext.Server.Console.ShowEndpoints) { + # state what endpoints are being listened on + Show-PodeConsoleEndpointsInfo -Force:$Force + } + if ($PodeContext.Server.Console.ShowOpenAPI) { + # state the OpenAPI endpoints for each definition + Show-PodeConsoleOAInfo -Force:$Force + } + } + + # Show help commands if enabled or hide them conditionally. + if ($showHelp) { + Show-PodeConsoleHelp + } + + elseif ($ctrlH ) { + Show-PodeConsoleHelp -Hide + } + + # Optionally display a footer separator. + if ($footerSeparator) { + # Write a horizontal divider line to the console. + Write-PodeHostDivider -Force $true + } +} + + +<# +.SYNOPSIS + Displays or hides the help section for Pode server control commands. + +.DESCRIPTION + The `Show-PodeConsoleHelp` function dynamically displays a list of control commands available for managing the Pode server. + Depending on the `$Hide` parameter, the help section can either be shown or hidden, with concise descriptions for each command. + Colors for headers, keys, and descriptions are customizable via the `$PodeContext.Server.Console.Colors` configuration. + +.PARAMETER Hide + Switch to display the "Show Help" option instead of the full help section. + +.PARAMETER Force + Overrides the -Quiet flag of the server. + +.PARAMETER Divider + Specifies the position of the divider: 'Header' or 'Footer'. + Default is 'Footer'. + +.NOTES + This function is designed for Pode's internal console display system and may change in future releases. + +.EXAMPLE + Show-PodeConsoleHelp + + Displays the full help section for the Pode server. + +.EXAMPLE + Show-PodeConsoleHelp -Hide + + Displays only the "Show Help" option instead of the full help section. + +#> +function Show-PodeConsoleHelp { + param( + [switch] + $Hide, + + [switch] + $Force, + + [string] + [ValidateSet('Header', 'Footer')] + $Divider = 'Footer' + ) + # Retrieve centralized key mapping for keyboard shortcuts + $KeyBindings = $PodeContext.Server.Console.KeyBindings + + # Define help section color variables + $helpHeaderColor = $PodeContext.Server.Console.Colors.HelpHeader + $helpKeyColor = $PodeContext.Server.Console.Colors.HelpKey + $helpDescriptionColor = $PodeContext.Server.Console.Colors.HelpDescription + $helpDividerColor = $PodeContext.Server.Console.Colors.HelpDivider + + # Add a header divider if specified + if ($Divider -eq 'Header') { + Write-PodeHostDivider -Force $Force + } + + # Display the "Show Help" option if the $Hide parameter is specified + if ($Hide) { + Write-PodeKeyBinding -Key $KeyBindings.Help -ForegroundColor $helpKeyColor -Force:$Force + # Message: 'Show Help' + Write-PodeHost $Podelocale.showHelpMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + else { + # Determine the text for resuming or suspending the server based on its state + + $resumeOrSuspend = if ($serverState -eq 'Suspended') { + $Podelocale.ResumeServerMessage + } + else { + $Podelocale.SuspendServerMessage + } + + # Determine whether to display "Enable" or "Disable Server" based on the server state + $enableOrDisable = if (Test-PodeServerIsEnabled) { $Podelocale.disableHttpServerMessage } else { $Podelocale.enableHttpServerMessage } + + # Display the header for the help section + Write-PodeHost $Podelocale.serverControlCommandsTitle -ForegroundColor $helpHeaderColor -Force:$Force + + if ($headerSeparator) { + # Write a horizontal divider line to the console. + Write-PodeHostDivider -Force $true + } + + # Display key bindings and their descriptions + if (!$PodeContext.Server.Console.DisableTermination) { + Write-PodeKeyBinding -Key $KeyBindings.Terminate -ForegroundColor $helpKeyColor -Force:$Force + Write-PodeHost "$($Podelocale.GracefullyTerminateMessage)" -ForegroundColor $helpDescriptionColor -Force:$Force + } + + if ($PodeContext.Server.AllowedActions.Restart) { + Write-PodeKeyBinding -Key $KeyBindings.Restart -ForegroundColor $helpKeyColor -Force:$Force + Write-PodeHost "$($Podelocale.RestartServerMessage)" -ForegroundColor $helpDescriptionColor -Force:$Force + } + + if ($PodeContext.Server.AllowedActions.Suspend) { + Write-PodeKeyBinding -Key $KeyBindings.Suspend -ForegroundColor $helpKeyColor -Force:$Force + Write-PodeHost "$resumeOrSuspend" -ForegroundColor $helpDescriptionColor -Force:$Force + } + + if (($serverState -eq 'Running') -and $PodeContext.Server.AllowedActions.Disable) { + Write-PodeKeyBinding -Key $KeyBindings.Disable -ForegroundColor $helpKeyColor -Force:$Force + # Message: 'Enable HTTP Server' or 'Disable HTTP Server' + Write-PodeHost $enableOrDisable -ForegroundColor $helpDescriptionColor -Force:$Force + } + + Write-PodeKeyBinding -Key $KeyBindings.Help -ForegroundColor $helpKeyColor -Force:$Force + # Message: 'Hide Help' + Write-PodeHost $Podelocale.hideHelpMessage -ForegroundColor $helpDescriptionColor -Force:$Force + + # If an HTTP endpoint exists and the server is running, display the browser shortcut + if ((Get-PodeEndpointUrl) -and ($serverState -ne 'Suspended')) { + Write-PodeKeyBinding -Key $KeyBindings.Browser -ForegroundColor $helpKeyColor -Force:$Force + # Message: Open the default HTTP endpoint in the default browser. + Write-PodeHost $Podelocale.OpenHttpEndpointMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + + # Display a divider for grouping commands + Write-PodeHost ' ----' -ForegroundColor $helpDividerColor -Force:$Force + + # Show metrics only if the server is running or suspended + if (('Running', 'Suspended') -contains $serverState ) { + Write-PodeKeyBinding -Key $KeyBindings.Metrics -ForegroundColor $helpKeyColor -Force:$Force + # Message: Show Metrics + Write-PodeHost $Podelocale.showMetricsMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + + # Show endpoints and OpenAPI only if the server is running + if ($serverState -eq 'Running') { + Write-PodeKeyBinding -Key $KeyBindings.Endpoints -ForegroundColor $helpKeyColor -Force:$Force + if ($PodeContext.Server.Console.ShowEndpoints) { + # Message: Hide Endpoints + Write-PodeHost $Podelocale.hideEndpointsMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + else { + # Message: Show Endpoints + Write-PodeHost $Podelocale.showEndpointsMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + + # Check if OpenAPI is enabled and display its toggle option + if (Test-PodeOAEnabled) { + Write-PodeKeyBinding -Key $KeyBindings.OpenAPI -ForegroundColor $helpKeyColor -Force:$Force + if ($PodeContext.Server.Console.ShowOpenAPI) { + # Message: Hide OpenAPI + Write-PodeHost $Podelocale.hideOpenAPIMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + else { + # Message: Show OpenAPI + Write-PodeHost $Podelocale.showOpenAPIMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + } + } + + # Display the Clear Console and Quiet Mode options + Write-PodeKeyBinding -Key $KeyBindings.Clear -ForegroundColor $helpKeyColor -Force:$Force + Write-PodeHost $Podelocale.clearConsoleMessage -ForegroundColor $helpDescriptionColor -Force:$Force + + Write-PodeKeyBinding -Key $KeyBindings.Quiet -ForegroundColor $helpKeyColor -Force:$Force + if ($PodeContext.Server.Console.Quiet) { + # Message: Disable Quiet Mode + Write-PodeHost $Podelocale.disableQuietModeMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + else { + # Message: Enable Quiet Mode + Write-PodeHost $Podelocale.enableQuietModeMessage -ForegroundColor $helpDescriptionColor -Force:$Force + } + + } + + # Add a footer divider if specified + if ($Divider -eq 'Footer') { + Write-PodeHostDivider -Force $Force + } +} + +<# +.SYNOPSIS + Writes a formatted key binding with "Ctrl+" prefix to the console. + +.DESCRIPTION + The `Write-PodeKeyBinding` function formats and displays a key binding in the console. + For digit keys (e.g., `D1`, `D2`), it removes the `D` prefix for better readability, + displaying them as `Ctrl+1`, `Ctrl+2`, etc. Other keys (e.g., `B`, `R`) are displayed as-is. + The output is colorized based on the provided foreground color. + +.PARAMETER Key + The key binding to display, as a string. Examples include `D1` for the `1` key, + or `B` for the `B` key. + +.PARAMETER ForegroundColor + The color to use for the key binding text in the console. + +.PARAMETER Force + Forces the console output to bypass any restrictions. This is useful for ensuring + the output is always displayed regardless of console constraints. + +.EXAMPLE + Write-PodeKeyBinding -Key 'D1' -ForegroundColor Yellow -Force + + Writes: "Ctrl+1 : " to the console in yellow text. + +.EXAMPLE + Write-PodeKeyBinding -Key 'B' -ForegroundColor Green + + Writes: "Ctrl+B : " to the console in green text. + +.NOTES + This function is specifically designed for Pode's internal console display system. + It simplifies the formatting of key bindings for easier understanding by end users. + Adjustments for non-standard keys can be added as needed. +#> +function Write-PodeKeyBinding { + param ( + # The key binding to display (e.g., 'D1', 'B') + [string]$Key, + + # The foreground color for the console text + [System.ConsoleColor] + $ForegroundColor, + + # Force writing to the console, even in restricted environments + [switch] + $Force + ) + + # Format the key binding: + # - Remove the "D" prefix for digit keys (D0-D9), displaying them as "Ctrl+1" instead of "Ctrl+D1" + # - Leave other keys (e.g., 'B', 'R') unchanged + $k = if ($Key -like 'D[0-9]') { + $Key.Substring(1) # Extract the digit part of the key (e.g., '1' from 'D1') + } + else { + $Key # Use the key as-is for non-digit keys + } + # Write the formatted key binding to the console + Write-PodeHost "$("Ctrl-$k".PadRight(8)): " -ForegroundColor $ForegroundColor -NoNewLine -Force:$Force +} + + +<# +.SYNOPSIS +Writes a visual divider line to the console. + +.DESCRIPTION +The `Write-PodeHostDivider` function outputs a horizontal divider line to the console. +For modern environments (PowerShell 6 and above or UTF-8 capable consoles), +it uses the `━` character repeated to form the divider. For older environments +like PowerShell 5.1, it falls back to the `-` character for compatibility. + +.PARAMETER Force +Forces the output to display the divider even if certain conditions are not met. + +.PARAMETER ForegroundColor +Specifies the foreground color of the divider. + +.EXAMPLE +Write-PodeHostDivider + +Writes a divider to the console using the appropriate characters for the environment. + +.EXAMPLE +Write-PodeHostDivider -Force $true + +Writes a divider to the console even if conditions for displaying it are not met. + +.NOTES +This function dynamically adapts to the PowerShell version and console encoding, ensuring compatibility across different environments. +#> +function Write-PodeHostDivider { + param ( + [bool]$Force = $false + ) + + if ($PodeContext.Server.Console.ShowDivider) { + if ($null -ne $PodeContext.Server.Console.Colors.Divider) { + $dividerColor = $PodeContext.Server.Console.Colors.Divider + } + else { + $dividerColor = [System.ConsoleColor]::Yellow + } + # Determine the divider style based on PowerShell version and encoding support + $dividerChar = if ($PSVersionTable.PSVersion.Major -ge 6 ) { + '━' * $PodeContext.Server.Console.DividerLength # Repeat the '━' character + } + else { + '-' * $PodeContext.Server.Console.DividerLength # Repeat the '-' as a fallback + } + + # Write the divider with the chosen style + Write-PodeHost $dividerChar -ForegroundColor $dividerColor -Force:$Force + } + else { + Write-PodeHost + } +} + +<# +.SYNOPSIS + Displays information about the endpoints the Pode server is listening on. + +.DESCRIPTION + The `Show-PodeConsoleEndpointsInfo` 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. + +.PARAMETER Force + Overrides the -Quiet flag of the server. + +.PARAMETER Divider + Specifies the position of the divider: 'Header' or 'Footer'. + Default is 'Footer'. + +.EXAMPLE + Show-PodeConsoleEndpointsInfo + + 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 the summary and other appropriate colors for URLs and flags. +#> +function Show-PodeConsoleEndpointsInfo { + param( + [switch] + $Force, + + [string] + [ValidateSet('Header', 'Footer')] + $Divider = 'Footer' + ) + + # Set default colors if not explicitly defined in PodeContext + if ($null -ne $PodeContext.Server.Console.Colors.EndpointsHeader) { + $headerColor = $PodeContext.Server.Console.Colors.EndpointsHeader + } + else { + $headerColor = [System.ConsoleColor]::Yellow + } + + if ($null -ne $PodeContext.Server.Console.Colors.Endpoints) { + $endpointsColor = $PodeContext.Server.Console.Colors.Endpoints + } + else { + $endpointsColor = [System.ConsoleColor]::Cyan + } + + if ($null -ne $PodeContext.Server.Console.Colors.EndpointsProtocol) { + $protocolsColor = $PodeContext.Server.Console.Colors.EndpointsProtocol + } + else { + $protocolsColor = [System.ConsoleColor]::White + } + + if ($null -ne $PodeContext.Server.Console.Colors.EndpointsFlag) { + $flagsColor = $PodeContext.Server.Console.Colors.EndpointsFlag + } + else { + $flagsColor = [System.ConsoleColor]::Gray + } + + if ($null -ne $PodeContext.Server.Console.Colors.EndpointsName) { + $nameColor = $PodeContext.Server.Console.Colors.EndpointsName + } + else { + $nameColor = [System.ConsoleColor]::Magenta + } + + # Exit early if no endpoints are available to display + if ($PodeContext.Server.EndpointsInfo.Length -eq 0) { + return + } + + # Add a header divider if specified + if ($Divider -eq 'Header') { + Write-PodeHostDivider -Force $Force + } + + # Group endpoints by protocol (e.g., HTTP, HTTPS) + $groupedEndpoints = $PodeContext.Server.EndpointsInfo | Group-Object { + ($_.Url -split ':')[0].ToUpper() + } + + # Calculate the maximum URL length for alignment of flags + $maxUrlLength = ($PodeContext.Server.EndpointsInfo | ForEach-Object { $_.Url.Length }) | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum + + # Display header with the total number of endpoints and threads + Write-PodeHost ($PodeLocale.listeningOnEndpointsMessage -f $PodeContext.Server.EndpointsInfo.Length, $PodeContext.Threads.General) -ForegroundColor $headerColor -Force:$Force + + # Write a divider line for visual separation + Write-PodeHostDivider -Force $true + + # Determine if the server is disabled + $disabled = ! (Test-PodeServerIsEnabled) + + # Loop through grouped endpoints by protocol + foreach ($group in $groupedEndpoints) { + # Define the protocol label with consistent spacing + $protocolLabel = switch ($group.Name) { + 'HTTP' { 'HTTP :' } + 'HTTPS' { 'HTTPS :' } + 'WS' { 'WS :' } + 'WSS' { 'WSS :' } + 'SMTP' { 'SMTP :' } + 'SMTPS' { 'SMTPS :' } + 'TCP' { 'TCP :' } + 'TCPS' { 'TCPS :' } + default { 'UNKNOWN' } + } + + # Flag to control whether the protocol label is displayed + $showGroupLabel = $true + foreach ($item in $group.Group) { + + # Display the protocol label only for the first item in the group + if ($showGroupLabel) { + Write-PodeHost " - $protocolLabel" -ForegroundColor $protocolsColor -Force:$Force -NoNewLine + $showGroupLabel = $false + } + else { + Write-PodeHost ' ' -Force:$Force -NoNewLine + } + + # Display the URL + Write-PodeHost " $($item.Url)" -ForegroundColor $endpointsColor -Force:$Force -NoNewLine + + # Prepare flags for the endpoint + $flags = @() + + # Add 'Disabled' flag if applicable + if ($disabled -and ('HTTP', 'HTTPS' -contains $group.Name)) { + $flags += 'Disabled' + } + + # Add Name flag if it doesn't match a GUID + if (![string]::IsNullOrEmpty($item.Name) -and ($item.Name -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')) { + $flags += "`b$($item.Name)" + } + + # Add remaining flags + if ($item.DualMode) { $flags += 'DualMode' } + if ($item.Default) { $flags += 'Default' } + + # Display flags if any are present + if ($flags.Count -gt 0) { + # Calculate padding dynamically + $urlPadding = $maxUrlLength - $item.Url.Length + 4 + Write-PodeHost $(' ' * $urlPadding + '[') -ForegroundColor $flagsColor -Force:$Force -NoNewLine + + $index = 0 + foreach ($flag in $flags) { + switch ($flag) { + { $flag[0] -eq [char]8 } { + # Display Name flag + Write-PodeHost 'Name: ' -ForegroundColor $flagsColor -Force:$Force -NoNewLine + Write-PodeHost "$flag" -ForegroundColor $nameColor -Force:$Force -NoNewLine + } + 'Disabled' { + # Display Disabled flag + Write-PodeHost 'Disabled' -ForegroundColor Yellow -Force:$Force -NoNewLine + } + default { + # Display other flags + Write-PodeHost "$flag" -ForegroundColor $flagsColor -Force:$Force -NoNewLine + } + } + + # Append comma if not the last flag + if (++$index -lt $flags.Length) { + Write-PodeHost ', ' -ForegroundColor $flagsColor -Force:$Force -NoNewLine + } + } + + # Close the flag block + Write-PodeHost ']' -ForegroundColor $flagsColor -Force:$Force + } + else { + # End line if no flags are present + Write-PodeHost + } + } + } + + # Add a footer divider if specified + if ($Divider -eq 'Footer') { + Write-PodeHostDivider -Force $Force + } +} + + + +<# +.SYNOPSIS + Displays metrics for the Pode server in the console. + +.DESCRIPTION + This function outputs various server metrics, such as uptime and restart counts, + to the Pode console with styled colors based on the Pode context. The function + ensures a visually clear representation of the metrics for quick monitoring. + +.EXAMPLE + Show-PodeConsoleMetric + + This command displays the Pode server metrics in the console with the + appropriate headers, labels, and values styled using Pode-defined colors. + +.NOTES + This function depends on the PodeContext and related server configurations + for retrieving metrics and console colors. Ensure that Pode is running and + configured correctly. + +.OUTPUTS + None. This function writes output directly to the console. + +#> +function Show-PodeConsoleMetric { + # Determine the color for the labels + $headerColor = $PodeContext.Server.Console.Colors.MetricsHeader + $labelColor = $PodeContext.Server.Console.Colors.MetricsLabel + $valueColor = $PodeContext.Server.Console.Colors.MetricsValue + + # Write a horizontal divider line to separate the header + Write-PodeHostDivider -Force $true + + # Write the metrics header with the current timestamp + Write-PodeHost "$($Podelocale.serverMetricsMessage) [$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')]" -ForegroundColor $headerColor + + # Write another horizontal divider line for separation + Write-PodeHostDivider -Force $true + + # Display the total uptime + Write-PodeHost "$($Podelocale.totalUptimeMessage) " -ForegroundColor $labelColor -NoNewLine + Write-PodeHost (Get-PodeServerUptime -Format Verbose -Total -ExcludeMilliseconds) -ForegroundColor $valueColor + + # If the server restarted, display uptime since last restart + if ((Get-PodeServerRestartCount) -gt 0) { + Write-PodeHost "$($Podelocale.uptimeSinceLastRestartMessage) "-ForegroundColor $labelColor -NoNewLine + Write-PodeHost (Get-PodeServerUptime -Format Verbose -ExcludeMilliseconds) -ForegroundColor $valueColor + } + + # Display the total number of server restarts + Write-PodeHost "$($Podelocale.totalRestartMessage) " -ForegroundColor $labelColor -NoNewLine + Write-PodeHost (Get-PodeServerRestartCount) -ForegroundColor $valueColor + + Write-PodeHost 'Requests' -ForegroundColor $labelColor + Write-PodeHost ' Total : ' -ForegroundColor $labelColor -NoNewLine + Write-PodeHost (Get-PodeServerActiveRequestMetric -CountType Total) -ForegroundColor $valueColor + Write-PodeHost ' Queued : ' -ForegroundColor $labelColor -NoNewLine + Write-PodeHost (Get-PodeServerActiveRequestMetric -CountType Queued) -ForegroundColor $valueColor + Write-PodeHost ' Processing : ' -ForegroundColor $labelColor -NoNewLine + Write-PodeHost (Get-PodeServerActiveRequestMetric -CountType Processing) -ForegroundColor $valueColor + +} + + +<# +.SYNOPSIS + Displays OpenAPI endpoint information for each definition in Pode. + +.DESCRIPTION + The `Show-PodeConsoleOAInfo` 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. + +.PARAMETER Force + Overrides the -Quiet flag of the server. + +.PARAMETER Divider + Specifies the position of the divider: 'Header' or 'Footer'. + Default is 'Footer'. + +.EXAMPLE + Show-PodeConsoleOAInfo + + This command will output the OpenAPI information for all definitions currently + configured in the Pode server, including specification and documentation URLs. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Show-PodeConsoleOAInfo { + param( + [switch] + $Force, + + [string] + [ValidateSet('Header', 'Footer')] + $Divider = 'Footer' + ) + + + # Default header initialization + $openAPIHeader = $false + + # Determine the color for the labels + $headerColor = $PodeContext.Server.Console.Colors.OpenApiHeaders + $titleColor = $PodeContext.Server.Console.Colors.OpenApiTitles + $subtitleColor = $PodeContext.Server.Console.Colors.OpenApiSubtitles + $urlColor = $PodeContext.Server.Console.Colors.OpenApiUrls + + + + # Iterate through OpenAPI definitions + foreach ($key in $PodeContext.Server.OpenAPI.Definitions.Keys) { + $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks + if (!$bookmarks) { + continue + } + + # Print the header only once + # Write-PodeHost -Force:$Force + if (!$openAPIHeader) { + + # Add a header divider if specified + if ($Divider -eq 'Header') { + Write-PodeHostDivider -Force $Force + } + + Write-PodeHost $PodeLocale.openApiInfoMessage -ForegroundColor $headerColor -Force:$Force + + # Write a horizontal divider line to the console. + Write-PodeHostDivider -Force $true + + $openAPIHeader = $true + } + + # Print definition title + Write-PodeHost " '$key':" -ForegroundColor $titleColor -Force:$Force + + # Determine endpoints for specification and documentation + if ($bookmarks.route.count -gt 1 -or $bookmarks.route.Endpoint.Name) { + # Directly use $bookmarks.route.Endpoint + Write-PodeHost " $($PodeLocale.specificationMessage):" -ForegroundColor $subtitleColor -Force:$Force + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.openApiUrl)" -ForegroundColor $urlColor -Force:$Force + } + Write-PodeHost " $($PodeLocale.documentationMessage):" -ForegroundColor $subtitleColor -Force:$Force + foreach ($endpoint in $bookmarks.route.Endpoint) { + Write-PodeHost " . $($endpoint.Protocol)://$($endpoint.Address)$($bookmarks.path)" -ForegroundColor $urlColor -Force:$Force + } + } + else { + # Use EndpointsInfo for fallback + Write-PodeHost " $($PodeLocale.specificationMessage):" -ForegroundColor $subtitleColor -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 $urlColor -Force:$Force + } + } + Write-PodeHost " $($PodeLocale.documentationMessage):" -ForegroundColor $subtitleColor -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 $urlColor -Force:$Force + } + } + } + } + # Add a footer divider if specified and OpenAPI is defined + if ($openAPIHeader -and ($Divider -eq 'Footer')) { + # Write a horizontal divider line to the console. + Write-PodeHostDivider -Force $true + } +} + +<# +.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 { + # Clear any remaining keys in the input buffer + while (![Console]::IsInputRedirected -and [Console]::KeyAvailable) { + $null = [Console]::ReadKey($true) + } +} + +<# +.SYNOPSIS + Tests if a specific key combination is pressed in the Pode console. + +.DESCRIPTION + This function checks if a key press matches a specified character and modifier combination. It supports detecting Control key presses on all platforms and Shift key presses on Unix systems. + +.PARAMETER Key + Optional. Specifies the key to test. If not provided, the function retrieves the key using `Get-PodeConsoleKey`. + +.PARAMETER Character + Mandatory. Specifies the character to test against the key press. + +.EXAMPLE + Test-PodeKeyPressed -Character 'C' + + Checks if the Control+C combination is pressed. + +.NOTES + This function is intended for use in scenarios where Pode's console input is enabled. +#> +function Test-PodeKeyPressed { + param( + [Parameter()] + $Key = $null, + + [Parameter(Mandatory = $true)] + [System.ConsoleKey] + $Character + ) + + # If console input is disabled, return false + if (($null -eq $Key) -or $PodeContext.Server.Console.DisableConsoleInput) { + return $false + } + + # Test the key press against the character and modifiers + return (($null -ne $Key) -and ($Key.Key -ieq $Character) -and + (($Key.Modifiers -band [ConsoleModifiers]::Control) -or ((Test-PodeIsUnix) -and ($Key.Modifiers -band [ConsoleModifiers]::Shift)))) +} + +<# +.SYNOPSIS + Gets the next key press from the Pode console. + +.DESCRIPTION + This function checks if a key is available in the console input buffer and retrieves it. If the console input is redirected or no key is available, the function returns `$null`. + +.EXAMPLE + Get-PodeConsoleKey + + Retrieves the next key press from the Pode console input buffer. + +.NOTES + This function is useful for scenarios requiring real-time console key handling. +#> +function Get-PodeConsoleKey { + try { + if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) { + return $null + } + + return [Console]::ReadKey($true) + } + finally { + Clear-PodeKeyPressed + } +} + +<# +.SYNOPSIS + Processes console actions and cancellation token triggers for the Pode server using a centralized key mapping. + +.DESCRIPTION + The `Invoke-PodeConsoleAction` function uses a hashtable to define and centralize key mappings, + allowing for easier updates and a cleaner implementation. + +.PARAMETER serverState + The current state of the Pode server, retrieved using Get-PodeServerState, + which determines whether actions like suspend, disable, or restart can be executed. + +.NOTES + This function is part of Pode's internal utilities and may change in future releases. + +.EXAMPLE + Invoke-PodeConsoleAction + + Processes the next key press or cancellation token to execute the corresponding server action. +#> +function Invoke-PodeConsoleAction { + param( + [Parameter(Mandatory = $true)] + [Pode.PodeServerState] + $ServerState + ) + # Get the next key press if console input is enabled + $Key = Get-PodeConsoleKey + if ($null -ne $key) { + if ($key.Modifiers -ne 'Control') { + return + } + else { + Write-Verbose "The Console received CTRL+$($key.Key)" + } + } + + # Centralized key mapping + $KeyBindings = $PodeContext.Server.Console.KeyBindings + + # Browser action + if (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Browser) { + # Open the browser + Show-PodeConsoleEndpointUrl + } + # Toggle help display + elseif (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Help) { + $PodeContext.Server.Console.ShowHelp = !$PodeContext.Server.Console.ShowHelp + if ($PodeContext.Server.Console.ShowHelp) { + Show-PodeConsoleHelp -Divider Footer + } + else { + Show-PodeConsoleInfo -ShowTopSeparator + } + } + # Toggle OpenAPI display + elseif (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.OpenAPI) { + $PodeContext.Server.Console.ShowOpenAPI = !$PodeContext.Server.Console.ShowOpenAPI + + if ($PodeContext.Server.Console.ShowOpenAPI) { + if (Test-PodeServerState -State Running) { + if ($PodeContext.Server.Console.ShowOpenAPI) { + # state what endpoints are being listened on + Show-PodeConsoleOAInfo -Force:$Force -Divider Footer + } + } + } + else { + Show-PodeConsoleInfo -ShowTopSeparator + } + } + # Toggle endpoints display + elseif (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Endpoints) { + $PodeContext.Server.Console.ShowEndpoints = !$PodeContext.Server.Console.ShowEndpoints + if ($PodeContext.Server.Console.ShowEndpoints) { + if (Test-PodeServerState -State Running) { + if ($PodeContext.Server.Console.ShowEndpoints) { + # state what endpoints are being listened on + Show-PodeConsoleEndpointsInfo -Force:$Force -Divider Footer + } + } + } + else { + Show-PodeConsoleInfo -ShowTopSeparator + } + } + # Clear console + elseif (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Clear) { + Show-PodeConsoleInfo -ClearHost + } + # Toggle quiet mode + elseif (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Quiet) { + $PodeContext.Server.Console.Quiet = !$PodeContext.Server.Console.Quiet + Show-PodeConsoleInfo -ClearHost -Force + } + # Show metrics + elseif ((([Pode.PodeServerState]::Running, [Pode.PodeServerState]::Suspended) -contains $serverState ) -and (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Metrics)) { + Show-PodeConsoleMetric + } + + # Handle restart actions + if ($PodeContext.Server.AllowedActions.Restart) { + if (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Restart) { + Close-PodeCancellationTokenRequest -Type Restart + Restart-PodeInternalServer + } + elseif (Test-PodeCancellationTokenRequest -Type Restart) { + Restart-PodeInternalServer + } + } + if (! $PodeContext.Server.Console.DisableTermination) { + # Terminate server + if ( (Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Terminate)) { + Close-PodeCancellationTokenRequest -Type Terminate + return + } + elseif ((Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Disable)) { + # Handle enable/disable server actions + if ($PodeContext.Server.AllowedActions.Disable -and ($serverState -eq [Pode.PodeServerState]::Running)) { + if (Test-PodeServerIsEnabled) { + Close-PodeCancellationTokenRequest -Type Disable + } + else { + Reset-PodeCancellationToken -Type Disable + } + + Write-PodeConsoleHeader -DisableHttp + + } + } + elseif ((Test-PodeKeyPressed -Key $Key -Character $KeyBindings.Suspend)) { + # Handle suspend/resume actions + if ($PodeContext.Server.AllowedActions.Suspend) { + if ($serverState -eq [Pode.PodeServerState]::Suspended) { + Set-PodeResumeToken + } + elseif ($serverState -eq [Pode.PodeServerState]::Running) { + Set-PodeSuspendToken + } + } + } + } +} + +<# +.SYNOPSIS + Retrieves the default console settings for Pode. + +.DESCRIPTION + The `Get-PodeDefaultConsole` function returns a hashtable containing the default console configuration for Pode. This includes settings for termination, console input, output formatting, timestamps, and color themes, as well as key bindings for console navigation. + +.OUTPUTS + [hashtable] + A hashtable representing the default console settings, including termination behavior, display options, colors, and key bindings. + +.EXAMPLE + $consoleSettings = Get-PodeDefaultConsole + Write-Output $consoleSettings + + This example retrieves the default console settings and displays them. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeDefaultConsole { + # Refer to https://learn.microsoft.com/en-us/dotnet/api/system.consolekey?view=net-9.0 for ConsoleKey Enum + if ($Host.Name -eq 'Visual Studio Code Host' ) { + $KeyBindings = @{ # Define custom key bindings for controls. + Browser = [System.ConsoleKey]::B # Open the default browser. + Help = [System.ConsoleKey]::F2 # Show/hide help instructions. + OpenAPI = [System.ConsoleKey]::F3 # Show/hide OpenAPI information. + Endpoints = [System.ConsoleKey]::F4 # Show/hide endpoints. + Clear = [System.ConsoleKey]::L # Clear the console output. + Quiet = [System.ConsoleKey]::F12 # Toggle quiet mode. + Terminate = [System.ConsoleKey]::C # Terminate the server. + Restart = [System.ConsoleKey]::F6 # Restart the server. + Disable = [System.ConsoleKey]::F7 # Disable the server. + Suspend = [System.ConsoleKey]::F9 # Suspend the server. + Metrics = [System.ConsoleKey]::F10 # Show Metrics. + } + } + else { + $KeyBindings = @{ # Define custom key bindings for controls. + Browser = [System.ConsoleKey]::B # Open the default browser. + Help = [System.ConsoleKey]::H # Show/hide help instructions. + OpenAPI = [System.ConsoleKey]::O # Show/hide OpenAPI information. + Endpoints = [System.ConsoleKey]::E # Show/hide endpoints. + Clear = [System.ConsoleKey]::L # Clear the console output. + Quiet = [System.ConsoleKey]::Q # Toggle quiet mode. + Terminate = [System.ConsoleKey]::C # Terminate the server. + Restart = [System.ConsoleKey]::R # Restart the server. + Disable = [System.ConsoleKey]::D # Disable the server. + Suspend = [System.ConsoleKey]::P # Suspend the server. + Metrics = [System.ConsoleKey]::M # Show Metrics. + } + } + return @{ + DisableTermination = $false # Prevent Ctrl+C from terminating the server. + DisableConsoleInput = $false # Disable all console input controls. + Quiet = $false # Suppress console output. + ClearHost = $false # Clear the console output at startup. + ShowOpenAPI = $true # Display OpenAPI information. + ShowEndpoints = $true # Display listening endpoints. + ShowHelp = $false # Show help instructions in the console. + ShowDivider = $true # Display dividers between sections. + DividerLength = 75 # Length of dividers in the console. + ShowTimeStamp = $true # Display timestamp in the header. + + Colors = @{ # Customize console colors. + Header = [System.ConsoleColor]::White # The server's header section, including the Pode version and timestamp. + EndpointsHeader = [System.ConsoleColor]::Yellow # The header for the endpoints list. + Endpoints = [System.ConsoleColor]::Cyan # The endpoints URLs. + EndpointsProtocol = [System.ConsoleColor]::White # The endpoints protocol. + EndpointsFlag = [System.ConsoleColor]::Gray # The endpoints flags. + EndpointsName = [System.ConsoleColor]::Magenta # The endpoints Name. + OpenApiUrls = [System.ConsoleColor]::Cyan # URLs listed under the OpenAPI information section. + OpenApiHeaders = [System.ConsoleColor]::Yellow # Section headers for OpenAPI information. + OpenApiTitles = [System.ConsoleColor]::White # The OpenAPI "default" title. + OpenApiSubtitles = [System.ConsoleColor]::Yellow # Subtitles under OpenAPI (e.g., Specification, Documentation). + HelpHeader = [System.ConsoleColor]::Yellow # Header for the Help section. + HelpKey = [System.ConsoleColor]::Green # Key bindings listed in the Help section (e.g., Ctrl+c). + HelpDescription = [System.ConsoleColor]::White # Descriptions for each Help section key binding. + HelpDivider = [System.ConsoleColor]::Gray # Dividers used in the Help section. + Divider = [System.ConsoleColor]::DarkGray # Dividers between console sections. + MetricsHeader = [System.ConsoleColor]::Yellow # Header for the Metric section. + MetricsLabel = [System.ConsoleColor]::White # Labels for values displayed in the Metrics section. + MetricsValue = [System.ConsoleColor]::Green # The actual values displayed in the Metrics section. + } + KeyBindings = $KeyBindings + } + +} + + +<# +.SYNOPSIS + Writes a formatted header to the Pode console with server details and status. + +.DESCRIPTION + The Write-PodeConsoleHeader function writes a customizable header line to the Pode console. + The header includes server details such as version, process ID (PID), and current status, + along with optional HTTP status information. It dynamically adjusts its output based on + the provided parameters and Pode context settings, including timestamp and colors. + +.PARAMETER Status + The status message to display in the header (e.g., Running, Suspended). + +.PARAMETER StatusColor + The color to use for the status message in the console. + +.PARAMETER NoNewLine + Prevents the addition of a newline after the header output. + +.PARAMETER Force + Forces the header to be written even if console restrictions are active. + +.PARAMETER DisableHttp + Displays HTTP status in the header, indicating whether HTTP is enabled or disabled. + +.NOTES + This is an internal function and may change in future releases of Pode. + It is used to format and display the header for the Pode server in the console. +#> +function Write-PodeConsoleHeader { + [CmdletBinding(DefaultParameterSetName = 'Status')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Status')] + [string] $Status, + + [Parameter(Mandatory = $true, ParameterSetName = 'Status')] + [System.ConsoleColor] $StatusColor, + + [switch] $NoNewLine, + + [switch] $Force, + + [Parameter(Mandatory = $true, ParameterSetName = 'DisableHttp')] + [switch] $DisableHttp + ) + + # Get the configured header color from Pode context. + $headerColor = $PodeContext.Server.Console.Colors.Header + + # If DisableHttp is set, override the Status and StatusColor parameters. + if ($DisableHttp) { + $Status = $Podelocale.runningMessage + $StatusColor = [System.ConsoleColor]::Green + } + + # Generate a timestamp if enabled in the Pode context. + $timestamp = if ($PodeContext.Server.Console.ShowTimeStamp) { + "[$([datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))]" + } + else { + '' + } + + # Write the header with timestamp, Pode version, and status. + Write-PodeHost "`r$timestamp Pode $(Get-PodeVersion) (PID: $($PID)) [" -ForegroundColor $headerColor -Force:$Force -NoNewLine + Write-PodeHost "$Status" -ForegroundColor $StatusColor -Force:$Force -NoNewLine + + if ($DisableHttp) { + # Append HTTP status to the header if DisableHttp is enabled. + Write-PodeHost '] - HTTP ' -ForegroundColor $headerColor -Force:$Force -NoNewLine + if (Test-PodeCancellationTokenRequest -Type Disable) { + Write-PodeHost 'Disabled' -ForegroundColor Yellow + } + else { + Write-PodeHost 'Enabled' -ForegroundColor Green + } + } + else { + # Close the header without HTTP status. + Write-PodeHost '] ' -ForegroundColor $headerColor -Force:$Force -NoNewLine:$NoNewLine + } +} + +<# +.SYNOPSIS + Sets override configurations for the Pode server console. + +.DESCRIPTION + This function updates the Pode server's console configuration by applying override settings based on the specified parameters. These configurations include disabling termination, console input, enabling quiet mode, and more. + +.PARAMETER DisableTermination + Sets an override to disable termination of the Pode server from the console. + +.PARAMETER DisableConsoleInput + Sets an override to disable input from the console for the Pode server. + +.PARAMETER Quiet + Sets an override to enable quiet mode, suppressing console output. + +.PARAMETER ClearHost + Sets an override to clear the host on startup. + +.PARAMETER HideOpenAPI + Sets an override to hide the OpenAPI documentation display. + +.PARAMETER HideEndpoints + Sets an override to hide the endpoints list display. + +.PARAMETER ShowHelp + Sets an override to show help information in the console. + +.PARAMETER Daemon + Sets an override to enable daemon mode, which combines quiet mode, disabled console input, and disabled termination. + +.NOTES + This function is used to dynamically apply override settings for the Pode server console configuration. +#> +function Set-PodeConsoleOverrideConfiguration { + param ( + [switch] + $DisableTermination, + + [switch] + $DisableConsoleInput, + + [switch] + $Quiet, + + [switch] + $ClearHost, + + [switch] + $HideOpenAPI, + + [switch] + $HideEndpoints, + + [switch] + $ShowHelp, + + [switch] + $Daemon + ) + + # Apply override settings + if ($DisableTermination.IsPresent) { + $PodeContext.Server.Console.DisableTermination = $true + } + if ($DisableConsoleInput.IsPresent) { + $PodeContext.Server.Console.DisableConsoleInput = $true + } + if ($Quiet.IsPresent) { + $PodeContext.Server.Console.Quiet = $true + } + if ($ClearHost.IsPresent) { + $PodeContext.Server.Console.ClearHost = $true + } + if ($HideOpenAPI.IsPresent) { + $PodeContext.Server.Console.ShowOpenAPI = $false + } + if ($HideEndpoints.IsPresent) { + $PodeContext.Server.Console.ShowEndpoints = $false + } + if ($ShowHelp.IsPresent) { + $PodeContext.Server.Console.ShowHelp = $true + } + if ($Daemon.IsPresent) { + $PodeContext.Server.Console.Quiet = $true + $PodeContext.Server.Console.DisableConsoleInput = $true + $PodeContext.Server.Console.DisableTermination = $true + } + + # Apply IIS-specific overrides + if ($PodeContext.Server.IsIIS) { + $PodeContext.Server.Console.DisableTermination = $true + $PodeContext.Server.Console.DisableConsoleInput = $true + + # If running under Azure Web App, enforce quiet mode + if (!(Test-PodeIsEmpty $env:WEBSITE_IIS_SITE_NAME)) { + $PodeContext.Server.Console.Quiet = $true + } + } +} + + +<# +.SYNOPSIS + Launches the Pode endpoint URL in the default browser. + +.DESCRIPTION + This function retrieves the URL of the Pode endpoint using Get-PodeEndpointUrl. If the URL is valid + and not null or whitespace, it triggers a browser event using Invoke-PodeEvent and opens the + URL in the system's default web browser using Start-Process. + +.EXAMPLE + Show-PodeConsoleEndpointUrl + This example retrieves the Pode endpoint URL and opens it in the default browser if available. + +.NOTES + Ensure the Pode endpoint is correctly set up and running. This function relies on the Pode framework. +#> + +function Show-PodeConsoleEndpointUrl { + $url = Get-PodeEndpointUrl + if (![string]::IsNullOrWhitespace($url)) { + Invoke-PodeEvent -Type Browser + Start-Process $url + } +} + +<# +.SYNOPSIS + Checks if the current PowerShell session supports console-like features. + +.DESCRIPTION + This function determines if the current PowerShell session is running in a host + that typically indicates a console-like environment where `Ctrl+C` can interrupt. + On Windows, it validates the standard input and output handles. + On non-Windows systems, it checks against known supported hosts. + +.OUTPUTS + [bool] + Returns `$true` if running in a console-like environment, `$false` otherwise. + +.EXAMPLE + Test-PodeHasConsole + # Returns `$true` if the session supports console-like behavior. +#> +function Test-PodeHasConsole { + + if (Test-PodeIsISEHost) { + return $true + } + + if (@('ConsoleHost', 'Visual Studio Code Host') -contains $Host.Name) { + + if (Test-PodeIsWindows) { + $handleTypeMap = @{ + Input = -10 + Output = -11 + Error = -12 + } + # On Windows, validate standard input and output handles + return ([Pode.NativeMethods]::IsHandleValid($handleTypeMap.Input) -and ` + [Pode.NativeMethods]::IsHandleValid($handleTypeMap.Output) -and ` + [Pode.NativeMethods]::IsHandleValid($handleTypeMap.Error) + ) + } + # On Linux or Mac + $handleTypeMap = @{ + Input = 0 + Output = 1 + Error = 2 + } + return ([Pode.NativeMethods]::IsTerminal($handleTypeMap.Input) -and ` + [Pode.NativeMethods]::IsTerminal($handleTypeMap.Output) -and ` + [Pode.NativeMethods]::IsTerminal($handleTypeMap.Error) + ) + } + return $false +} + +<# +.SYNOPSIS + Determines if the current PowerShell session is running in the ConsoleHost. + +.DESCRIPTION + This function checks if the session's host name matches 'ConsoleHost', + which typically represents a native terminal environment in PowerShell. + +.OUTPUTS + [bool] + Returns `$true` if the current host is 'ConsoleHost', otherwise `$false`. + +.EXAMPLE + Test-PodeIsConsoleHost + # Returns `$true` if running in ConsoleHost, `$false` otherwise. +#> +function Test-PodeIsConsoleHost { + return $Host.Name -eq 'ConsoleHost' +} \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 288d4f0ab..54dfbb8b3 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -43,14 +43,17 @@ function New-PodeContext { [string[]] $EnablePool, - [switch] - $DisableTermination, + [hashtable] + $Console, [switch] - $Quiet, + $EnableBreakpoints, [switch] - $EnableBreakpoints + $IgnoreServerConfig, + + [string] + $ConfigFile ) # set a random server name if one not supplied @@ -92,8 +95,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 @@ -190,8 +192,40 @@ function New-PodeContext { 'Errors' = 'errors' } - # check if there is any global configuration - $ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx + $ctx.Server.Debug = @{ + Breakpoints = @{ + Enabled = $false + } + } + + $ctx.Server.AllowedActions = @{ + Suspend = $true + Restart = $true + Disable = $true + DisableSettings = @{ + RetryAfter = 3600 + MiddlewareName = '__Pode_Midleware_Code_503__' + } + Timeout = @{ + Suspend = 30 + Resume = 30 + } + } + + # Load the server configuration based on the provided parameters. + # If $IgnoreServerConfig is set, an empty configuration (@{}) is assigned; otherwise, the configuration is loaded using Open-PodeConfiguration. + $ctx.Server.Configuration = if ($IgnoreServerConfig) { @{} } + else { + Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx -ConfigFile $ConfigFile + } + + # Set the 'Enabled' property of the server configuration. + # This is based on whether $IgnoreServerConfig is explicitly present (false if present, true otherwise). + $ctx.Server.Configuration.Enabled = ! $IgnoreServerConfig.IsPresent + + # Assign the specified configuration file path (if any) to the 'ConfigFile' property of the server configuration. + # This allows tracking which configuration file was used, even if overridden. + $ctx.Server.Configuration.ConfigFile = $ConfigFile # over status page exceptions if (!(Test-PodeIsEmpty $StatusPageExceptions)) { @@ -214,10 +248,6 @@ function New-PodeContext { # debugging if ($EnableBreakpoints) { - if ($null -eq $ctx.Server.Debug) { - $ctx.Server.Debug = @{ Breakpoints = @{} } - } - $ctx.Server.Debug.Breakpoints.Enabled = $EnableBreakpoints.IsPresent } @@ -228,7 +258,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 @@ -238,13 +268,6 @@ 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 - - # if under IIS and Azure Web App, force quiet - if (!(Test-PodeIsEmpty $env:WEBSITE_IIS_SITE_NAME)) { - $ctx.Server.Quiet = $true - } - # set iis token/settings $ctx.Server.IIS = @{ Token = $env:ASPNETCORE_TOKEN @@ -267,9 +290,42 @@ function New-PodeContext { # is the server running under Heroku? $ctx.Server.IsHeroku = (!$isServerless -and (!(Test-PodeIsEmpty $env:PORT)) -and (!(Test-PodeIsEmpty $env:DYNO))) - # if we're inside a remote host, stop termination - if ($Host.Name -ieq 'ServerRemoteHost') { - $ctx.Server.DisableTermination = $true + # Check if the current session is running in a console-like environment + if (Test-PodeHasConsole) { + try { + if (! (Test-PodeIsISEHost)) { + # If the session is not configured for quiet mode, modify console behavior + if (!$ctx.Server.Console.Quiet) { + # Hide the cursor to improve the console appearance + [System.Console]::CursorVisible = $false + + # If the divider line should be shown, configure UTF-8 output encoding + if ($ctx.Server.Console.ShowDivider) { + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + } + } + if (Test-PodeIsConsoleHost) { + # Treat Ctrl+C as input instead of terminating the process + [Console]::TreatControlCAsInput = $true + } + } + } + catch { + $_ | Write-PodeErrorLog + # Console support is partial , configure the context for non-console behavior + $ctx.Server.Console.DisableTermination = $true # Prevent termination + $ctx.Server.Console.DisableConsoleInput = $true # Disable console input + $ctx.Server.Console.Quiet = $true # Silence the console + $ctx.Server.Console.ShowDivider = $false # Disable divider display + } + + } + else { + # If not running in a console-like environment, configure the context for non-console behavior + $ctx.Server.Console.DisableTermination = $true # Prevent termination + $ctx.Server.Console.DisableConsoleInput = $true # Disable console input + $ctx.Server.Console.Quiet = $true # Silence the console + $ctx.Server.Console.ShowDivider = $false # Disable divider display } # set the IP address details @@ -320,13 +376,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]@{} @@ -379,6 +435,7 @@ function New-PodeContext { #OpenApi Definition Tag $ctx.Server.OpenAPI = Initialize-PodeOpenApiTable -DefaultDefinitionTag $ctx.Server.Web.OpenApi.DefaultDefinitionTag + # server metrics $ctx.Metrics = @{ Server = @{ @@ -405,10 +462,7 @@ function New-PodeContext { } # create new cancellation tokens - $ctx.Tokens = @{ - Cancellation = [System.Threading.CancellationTokenSource]::new() - Restart = [System.Threading.CancellationTokenSource]::new() - } + $ctx.Tokens = Initialize-PodeCancellationToken # requests that should be logged $ctx.LogsToProcess = [System.Collections.ArrayList]::new() @@ -545,47 +599,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 @@ -594,40 +654,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' @@ -800,23 +865,52 @@ function New-PodeStateContext { Server = $Context.Server } } +<# +.SYNOPSIS + Opens and processes the Pode server configuration. + +.DESCRIPTION + This function handles loading the Pode server configuration file. It supports custom configurations specified by environment variables, + a provided file path, or falls back to the default `server.psd1` file. The function sets the configuration for both the server and web contexts. +.PARAMETER ServerRoot + Specifies the root directory of the server. Defaults to `$null` if not provided. + +.PARAMETER Context + Specifies the context to set configurations for Pode server and web. + +.PARAMETER ConfigFile + Allows specifying a custom configuration file path. If provided, it overrides any other configuration file. + +.OUTPUTS + Hashtable representing the loaded configuration. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> function Open-PodeConfiguration { + [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter()] [string] $ServerRoot = $null, [Parameter()] - $Context + $Context, + + [Parameter()] + [string] + $ConfigFile ) + # Initialize an empty configuration hashtable $config = @{} - # set the path to the root config file + # Set the path to the default root configuration file $configPath = (Join-PodeServerRoot -Folder '.' -FilePath 'server.psd1' -Root $ServerRoot) - # check to see if an environmental config exists (if the env var is set) + # Check for an environment-specific configuration file if the environment variable is set if (!(Test-PodeIsEmpty $env:PODE_ENVIRONMENT)) { $_path = (Join-PodeServerRoot -Folder '.' -FilePath "server.$($env:PODE_ENVIRONMENT).psd1" -Root $ServerRoot) if (Test-PodePath -Path $_path -NoStatus) { @@ -824,13 +918,23 @@ function Open-PodeConfiguration { } } + # Override the configuration path if a valid ConfigFile parameter is provided + if (!([string]::IsNullOrEmpty($ConfigFile))) { + #-and (Test-Path -Path $ConfigFile -PathType Leaf)) { + $configPath = Get-PodeRelativePath -Path $ConfigFile -JoinRoot -Resolve -RootPath $ServerRoot -TestPath + } + # check the path exists, and load the config if (Test-PodePath -Path $configPath -NoStatus) { + # Import the configuration from the file $config = Import-PowerShellDataFile -Path $configPath -ErrorAction Stop + + # Set the server and web configurations in the provided context Set-PodeServerConfiguration -Configuration $config.Server -Context $Context Set-PodeWebConfiguration -Configuration $config.Web -Context $Context } + # Return the loaded configuration return $config } @@ -900,9 +1004,73 @@ 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) + } + } + + $Context.Server.AllowedActions = @{ + Suspend = [bool](Protect-PodeValue -Value $Configuration.AllowedActions.Suspend -Default $Context.Server.AllowedActions.Suspend) + Restart = [bool](Protect-PodeValue -Value $Configuration.AllowedActions.Restart -Default $Context.Server.AllowedActions.Restart) + Disable = [bool](Protect-PodeValue -Value $Configuration.AllowedActions.Disable -Default $Context.Server.AllowedActions.Disable) + DisableSettings = @{ + RetryAfter = [int](Protect-PodeValue -Value $Configuration.AllowedActions.DisableSettings.RetryAfter -Default $Context.Server.AllowedActions.DisableSettings.RetryAfter) + MiddlewareName = (Protect-PodeValue -Value $Configuration.AllowedActions.DisableSettings.MiddlewareName -Default $Context.Server.AllowedActions.DisableSettings.MiddlewareName) + } + Timeout = @{ + Suspend = [int](Protect-PodeValue -Value $Configuration.AllowedActions.Timeout.Suspend -Default $Context.Server.AllowedActions.Timeout.Suspend) + Resume = [int](Protect-PodeValue -Value $Configuration.AllowedActions.Timeout.Resume -Default $Context.Server.AllowedActions.Timeout.Resume) } } + + $Context.Server.Console = @{ + DisableTermination = [bool](Protect-PodeValue -Value $Configuration.Console.DisableTermination -Default $Context.Server.Console.DisableTermination) + DisableConsoleInput = [bool](Protect-PodeValue -Value $Configuration.Console.DisableConsoleInput -Default $Context.Server.Console.DisableConsoleInput) + Quiet = [bool](Protect-PodeValue -Value $Configuration.Console.Quiet -Default $Context.Server.Console.Quiet) + ClearHost = [bool](Protect-PodeValue -Value $Configuration.Console.ClearHost -Default $Context.Server.Console.ClearHost) + ShowOpenAPI = [bool](Protect-PodeValue -Value $Configuration.Console.ShowOpenAPI -Default $Context.Server.Console.ShowOpenAPI) + ShowEndpoints = [bool](Protect-PodeValue -Value $Configuration.Console.ShowEndpoints -Default $Context.Server.Console.ShowEndpoints) + ShowHelp = [bool](Protect-PodeValue -Value $Configuration.Console.ShowHelp -Default $Context.Server.Console.ShowHelp) + ShowDivider = [bool](Protect-PodeValue -Value $Configuration.Console.ShowDivider -Default $Context.Server.Console.ShowDivider) + ShowTimeStamp = [bool](Protect-PodeValue -Value $Configuration.Console.ShowTimeStamp -Default $Context.Server.Console.ShowTimeStamp) + DividerLength = [int](Protect-PodeValue -Value $Configuration.Console.DividerLength -Default $Context.Server.Console.DividerLength) + Colors = @{ + Header = Protect-PodeValue $Configuration.Console.Colors.Header -Default $Context.Server.Console.Colors.Header -EnumType ([type][System.ConsoleColor]) + EndpointsHeader = Protect-PodeValue -Value $Configuration.Console.Colors.EndpointsHeader -Default $Context.Server.Console.Colors.EndpointsHeader -EnumType ([type][System.ConsoleColor]) + Endpoints = Protect-PodeValue -Value $Configuration.Console.Colors.Endpoints -Default $Context.Server.Console.Colors.Endpoints -EnumType ([type][System.ConsoleColor]) + EndpointsProtocol = Protect-PodeValue -Value $Configuration.Console.Colors.EndpointsProtocol -Default $Context.Server.Console.Colors.EndpointsProtocol -EnumType ([type][System.ConsoleColor]) + EndpointsFlag = Protect-PodeValue -Value $Configuration.Console.Colors.EndpointsFlag -Default $Context.Server.Console.Colors.EndpointsFlag -EnumType ([type][System.ConsoleColor]) + EndpointsName = Protect-PodeValue -Value $Configuration.Console.Colors.EndpointsName -Default $Context.Server.Console.Colors.EndpointsName -EnumType ([type][System.ConsoleColor]) + OpenApiUrls = Protect-PodeValue -Value $Configuration.Console.Colors.OpenApiUrls -Default $Context.Server.Console.Colors.OpenApiUrls -EnumType ([type][System.ConsoleColor]) + OpenApiHeaders = Protect-PodeValue -Value $Configuration.Console.Colors.OpenApiHeaders -Default $Context.Server.Console.Colors.OpenApiHeaders -EnumType ([type][System.ConsoleColor]) + OpenApiTitles = Protect-PodeValue -Value $Configuration.Console.Colors.OpenApiTitles -Default $Context.Server.Console.Colors.OpenApiTitles -EnumType ([type][System.ConsoleColor]) + OpenApiSubtitles = Protect-PodeValue -Value $Configuration.Console.Colors.OpenApiSubtitles -Default $Context.Server.Console.Colors.OpenApiSubtitles -EnumType ([type][System.ConsoleColor]) + HelpHeader = Protect-PodeValue -Value $Configuration.Console.Colors.HelpHeader -Default $Context.Server.Console.Colors.HelpHeader -EnumType ([type][System.ConsoleColor]) + HelpKey = Protect-PodeValue -Value $Configuration.Console.Colors.HelpKey -Default $Context.Server.Console.Colors.HelpKey -EnumType ([type][System.ConsoleColor]) + HelpDescription = Protect-PodeValue -Value $Configuration.Console.Colors.HelpDescription -Default $Context.Server.Console.Colors.HelpDescription -EnumType ([type][System.ConsoleColor]) + HelpDivider = Protect-PodeValue -Value $Configuration.Console.Colors.HelpDivider -Default $Context.Server.Console.Colors.HelpDivider -EnumType ([type][System.ConsoleColor]) + Divider = Protect-PodeValue -Value $Configuration.Console.Colors.Divider -Default $Context.Server.Console.Colors.Divider -EnumType ([type][System.ConsoleColor]) + MetricsHeader = Protect-PodeValue -Value $Configuration.Console.Colors.MetricsHeader -Default $Context.Server.Console.Colors.MetricsHeader -EnumType ([type][System.ConsoleColor]) + MetricsLabel = Protect-PodeValue -Value $Configuration.Console.Colors.MetricsLabel -Default $Context.Server.Console.Colors.MetricsLabel -EnumType ([type][System.ConsoleColor]) + MetricsValue = Protect-PodeValue -Value $Configuration.Console.Colors.MetricsValue -Default $Context.Server.Console.Colors.MetricsValue -EnumType ([type][System.ConsoleColor]) + + + } + KeyBindings = @{ + Browser = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Browser -Default $Context.Server.Console.KeyBindings.Browser -EnumType ([type][System.ConsoleKey]) + Help = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Help -Default $Context.Server.Console.KeyBindings.Help -EnumType ([type][System.ConsoleKey]) + OpenAPI = Protect-PodeValue -Value $Configuration.Console.KeyBindings.OpenAPI -Default $Context.Server.Console.KeyBindings.OpenAPI -EnumType ([type][System.ConsoleKey]) + Endpoints = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Endpoints -Default $Context.Server.Console.KeyBindings.Endpoints -EnumType ([type][System.ConsoleKey]) + Clear = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Clear -Default $Context.Server.Console.KeyBindings.Clear -EnumType ([type][System.ConsoleKey]) + Quiet = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Quiet -Default $Context.Server.Console.KeyBindings.Quiet -EnumType ([type][System.ConsoleKey]) + Terminate = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Terminate -Default $Context.Server.Console.KeyBindings.Terminate -EnumType ([type][System.ConsoleKey]) + Restart = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Restart -Default $Context.Server.Console.KeyBindings.Restart -EnumType ([type][System.ConsoleKey]) + Disable = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Disable -Default $Context.Server.Console.KeyBindings.Disable -EnumType ([type][System.ConsoleKey]) + Suspend = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Suspend -Default $Context.Server.Console.KeyBindings.Suspend -EnumType ([type][System.ConsoleKey]) + Metrics = Protect-PodeValue -Value $Configuration.Console.KeyBindings.Metrics -Default $Context.Server.Console.KeyBindings.Metrics -EnumType ([type][System.ConsoleKey]) + } + } + + } function Set-PodeWebConfiguration { @@ -992,7 +1160,7 @@ function New-PodeAutoRestartServer { $period = [int]$restart.period if ($period -gt 0) { Add-PodeTimer -Name '__pode_restart_period__' -Interval ($period * 60) -ScriptBlock { - $PodeContext.Tokens.Restart.Cancel() + Close-PodeCancellationTokenRequest -Type Restart } } @@ -1007,7 +1175,7 @@ function New-PodeAutoRestartServer { } Add-PodeSchedule -Name '__pode_restart_times__' -Cron @($crons) -ScriptBlock { - $PodeContext.Tokens.Restart.Cancel() + Close-PodeCancellationTokenRequest -Type Restart } } @@ -1015,7 +1183,7 @@ function New-PodeAutoRestartServer { $crons = @(@($restart.crons) -ne $null) if (($crons | Measure-Object).Count -gt 0) { Add-PodeSchedule -Name '__pode_restart_crons__' -Cron @($crons) -ScriptBlock { - $PodeContext.Tokens.Restart.Cancel() + Close-PodeCancellationTokenRequest -Type Restart } } } diff --git a/src/Private/Endpoints.ps1 b/src/Private/Endpoints.ps1 index 7cc6cf657..5927052d5 100644 --- a/src/Private/Endpoints.ps1 +++ b/src/Private/Endpoints.ps1 @@ -385,4 +385,64 @@ function Get-PodeEndpointByName { } return $null -} \ No newline at end of file +} + +<# +.SYNOPSIS + Organizes the Pode server's endpoint list based on protocol and URL. + +.DESCRIPTION + This internal utility function arranges an array of endpoint hashtables, sorting them + first by protocol in a predefined order and then alphabetically by URL. It ensures + a consistent structure for subsequent processing or display. + +.PARAMETER EndpointsInfo + An array of hashtables representing endpoint details, with fields such as `Url`, + `DualMode`, and `Pool`. + +.OUTPUTS + An array of endpoints organized for consistency. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeSortedEndpointsInfo { + param( + [Parameter(Mandatory = $true)] + [array] + $EndpointsInfo + ) + + #$PodeContext.Server.EndpointsInfo + + # Define protocol sorting order + $protocolOrder = @{ + 'HTTP' = 1 + 'HTTPS' = 2 + 'WS' = 3 + 'WSS' = 4 + 'SMTP' = 5 + 'SMTPS' = 6 + 'TCP' = 7 + 'TCPS' = 8 + } + + # Add protocol field to each endpoint for sorting + $formattedEndpoints = $EndpointsInfo | ForEach-Object { + $protocol = ($_.Url -split ':')[0].ToUpper() + @{ + Protocol = $protocol + DualMode = $_.DualMode + Pool = $_.Pool + Url = $_.Url + Name = $_.Name + Default = $_.Default + Order = $protocolOrder[$protocol] -as [int] + + } + } + + # Sort endpoints by protocol order and then by URL + return $formattedEndpoints | Sort-Object -Property @{Expression = 'Order'; Ascending = $true }, @{Expression = 'Url'; Ascending = $true } + +} diff --git a/src/Private/Events.ps1 b/src/Private/Events.ps1 index 592f51817..fb9334572 100644 --- a/src/Private/Events.ps1 +++ b/src/Private/Events.ps1 @@ -1,8 +1,7 @@ function Invoke-PodeEvent { param( [Parameter(Mandatory = $true)] - [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')] - [string] + [Pode.PodeServerEventType] $Type ) 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/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index ce0ebc4c7..97c590ad4 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -60,80 +60,86 @@ function Start-PodeFileWatcherRunspace { [int] $ThreadId ) + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start + do { + try { + while ($Watcher.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { + $evt = (Wait-PodeTask -Task $Watcher.GetFileEventAsync($PodeContext.Tokens.Cancellation.Token)) - try { - while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - $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 token and wait for the debugger to reset if active + } 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 @@ -144,7 +150,7 @@ function Start-PodeFileWatcherRunspace { ) try { - while ($Watcher.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Watcher.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { Start-Sleep -Seconds 1 } } diff --git a/src/Private/Gui.ps1 b/src/Private/Gui.ps1 index 94b772795..d8135389c 100644 --- a/src/Private/Gui.ps1 +++ b/src/Private/Gui.ps1 @@ -12,6 +12,9 @@ function Start-PodeGuiRunspace { } $script = { + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start + try { # if there are multiple endpoints, flag warning we're only using the first - unless explicitly set if ($null -eq $PodeContext.Server.Gui.Endpoint) { @@ -27,7 +30,7 @@ function Start-PodeGuiRunspace { # poll the server for a response $count = 0 - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { try { $null = Invoke-WebRequest -Method Get -Uri $uri -UseBasicParsing -ErrorAction Stop if (!$?) { @@ -132,7 +135,7 @@ function Start-PodeGuiRunspace { } finally { # invoke the cancellation token to close the server - $PodeContext.Tokens.Cancellation.Cancel() + Close-PodeCancellationTokenRequest -Type Cancellation } } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 8e4e9f37d..7e57ba7b0 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -551,106 +551,47 @@ function Get-PodeSubnetRange { } } - -function Get-PodeConsoleKey { - if ([Console]::IsInputRedirected -or ![Console]::KeyAvailable) { - return $null - } - - return [Console]::ReadKey($true) -} - -function Test-PodeTerminationPressed { - param( - [Parameter()] - $Key = $null - ) - - if ($PodeContext.Server.DisableTermination) { - return $false - } - - return (Test-PodeKeyPressed -Key $Key -Character 'c') -} - -function Test-PodeRestartPressed { - param( - [Parameter()] - $Key = $null - ) - - return (Test-PodeKeyPressed -Key $Key -Character 'r') -} - -function Test-PodeOpenBrowserPressed { - param( - [Parameter()] - $Key = $null - ) - - return (Test-PodeKeyPressed -Key $Key -Character 'b') -} - -function Test-PodeKeyPressed { - param( - [Parameter()] - $Key = $null, - - [Parameter(Mandatory = $true)] - [string] - $Character - ) - - if ($null -eq $Key) { - $Key = Get-PodeConsoleKey - } - - return (($null -ne $Key) -and ($Key.Key -ieq $Character) -and - (($Key.Modifiers -band [ConsoleModifiers]::Control) -or ((Test-PodeIsUnix) -and ($Key.Modifiers -band [ConsoleModifiers]::Shift)))) -} - function Close-PodeServerInternal { - param( - [switch] - $ShowDoneMessage - ) - - # ensure the token is cancelled - if ($null -ne $PodeContext.Tokens.Cancellation) { + # PodeContext doesn't exist return + if ($null -eq $PodeContext) { return } + try { + # ensure the token is cancelled Write-Verbose 'Cancelling main cancellation token' - $PodeContext.Tokens.Cancellation.Cancel() - } + Close-PodeCancellationTokenRequest -Type Cancellation, Terminate - # stop all current runspaces - Write-Verbose 'Closing runspaces' - Close-PodeRunspace -ClosePool + # stop all current runspaces + Write-Verbose 'Closing runspaces' + Close-PodeRunspace -ClosePool - # stop the file monitor if it's running - Write-Verbose 'Stopping file monitor' - Stop-PodeFileMonitor + # stop the file monitor if it's running + Write-Verbose 'Stopping file monitor' + Stop-PodeFileMonitor - try { - # remove all the cancellation tokens - Write-Verbose 'Disposing cancellation tokens' - Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation - Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart + try { + # remove all the cancellation tokens + Write-Verbose 'Disposing cancellation tokens' + Close-PodeCancellationToken #-Type Cancellation, Terminate, Restart, Suspend, Resume, Start - # dispose mutex/semaphores - Write-Verbose 'Diposing mutex and semaphores' - Clear-PodeMutexes - Clear-PodeSemaphores + # dispose mutex/semaphores + Write-Verbose 'Diposing mutex and semaphores' + Clear-PodeMutexes + Clear-PodeSemaphores + } + catch { + $_ | Out-Default + } + + # remove all of the pode temp drives + Write-Verbose 'Removing internal PSDrives' + Remove-PodePSDrive } - catch { - $_ | Out-Default + finally { + if ($null -ne $PodeContext) { + # Remove any tokens + $PodeContext.Tokens = $null + } } - # remove all of the pode temp drives - Write-Verbose 'Removing internal PSDrives' - Remove-PodePSDrive - - if ($ShowDoneMessage -and ($PodeContext.Server.Types.Length -gt 0) -and !$PodeContext.Server.IsServerless) { - Write-PodeHost $PodeLocale.doneMessage -ForegroundColor Green - } } function New-PodePSDrive { @@ -2465,6 +2406,43 @@ function Find-PodeFileForContentType { 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. + +.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 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)] @@ -2483,6 +2461,7 @@ function Get-PodeRelativePath { [switch] $TestPath + ) # if the path is relative, join to root if flagged @@ -2600,6 +2579,10 @@ function Get-PodeEndpointUrl { } } + if ($null -eq $Endpoint) { + return $null + } + $url = $Endpoint.Url if ([string]::IsNullOrWhiteSpace($url)) { $url = "$($Endpoint.Protocol)://$($Endpoint.FriendlyName):$($Endpoint.Port)" @@ -3766,4 +3749,184 @@ 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) } -} \ No newline at end of file +} + +<# +.SYNOPSIS + Converts a duration in milliseconds into a human-readable time format. + +.DESCRIPTION + The `Convert-PodeMillisecondsToReadable` function converts a specified duration in milliseconds into + a readable time format. The output can be formatted in three styles: + - `Concise`: A short and simple format (e.g., "1d 2h 3m"). + - `Compact`: A compact representation (e.g., "01:02:03:04"). + - `Verbose`: A detailed, descriptive format (e.g., "1 day, 2 hours, 3 minutes"). + The function also provides an option to exclude milliseconds from the output for all formats. + +.PARAMETER Milliseconds + Specifies the duration in milliseconds to be converted into a human-readable format. + +.PARAMETER Format + Specifies the desired format for the output. Valid options are: + - `Concise` (default): Short and simple (e.g., "1d 2h 3m"). + - `Compact`: Condensed form (e.g., "01:02:03:04"). + - `Verbose`: Detailed description (e.g., "1 day, 2 hours, 3 minutes, 4 seconds"). + +.PARAMETER ExcludeMilliseconds + If specified, milliseconds will be excluded from the output for all formats. + +.EXAMPLE + Convert-PodeMillisecondsToReadable -Milliseconds 123456789 + + Output: + 1d 10h 17m 36s + +.EXAMPLE + Convert-PodeMillisecondsToReadable -Milliseconds 123456789 -Format Verbose + + Output: + 1 day, 10 hours, 17 minutes, 36 seconds, 789 milliseconds + +.EXAMPLE + Convert-PodeMillisecondsToReadable -Milliseconds 123456789 -Format Compact -ExcludeMilliseconds + + Output: + 01:10:17:36 + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Convert-PodeMillisecondsToReadable { + param( + # The duration in milliseconds to convert + [Parameter(Mandatory = $true)] + [long] + $Milliseconds, + + # Specifies the desired output format + [Parameter()] + [ValidateSet('Concise', 'Compact', 'Verbose')] + [string] + $Format = 'Concise', + + # Omits milliseconds from the output + [switch] + $ExcludeMilliseconds + ) + + # Convert the milliseconds input into a TimeSpan object + $timeSpan = [timespan]::FromMilliseconds($Milliseconds) + + # Generate the formatted output based on the selected format + switch ($Format.ToLower()) { + 'concise' { + # Concise format: "1d 2h 3m 4s" + $output = @() + if ($timeSpan.Days -gt 0) { $output += "$($timeSpan.Days)d" } + if ($timeSpan.Hours -gt 0) { $output += "$($timeSpan.Hours)h" } + if ($timeSpan.Minutes -gt 0) { $output += "$($timeSpan.Minutes)m" } + if ($timeSpan.Seconds -gt 0) { $output += "$($timeSpan.Seconds)s" } + + # Include milliseconds if they exist and are not excluded + if ((($timeSpan.Milliseconds -gt 0) -and !$ExcludeMilliseconds) -or ($output.Count -eq 0)) { + $output += "$($timeSpan.Milliseconds)ms" + } + + return $output -join ' ' + } + + 'compact' { + # Compact format: "dd:hh:mm:ss" + $output = '{0:D2}:{1:D2}:{2:D2}:{3:D2}' -f $timeSpan.Days, $timeSpan.Hours, $timeSpan.Minutes, $timeSpan.Seconds + + # Append milliseconds if not excluded + if (!$ExcludeMilliseconds) { + $output += '.{0:D3}' -f $timeSpan.Milliseconds + } + + return $output + } + + 'verbose' { + # Verbose format: "1 day, 2 hours, 3 minutes, 4 seconds" + $output = @() + if ($timeSpan.Days -gt 0) { $output += "$($timeSpan.Days) day$(if ($timeSpan.Days -ne 1) { 's' })" } + if ($timeSpan.Hours -gt 0) { $output += "$($timeSpan.Hours) hour$(if ($timeSpan.Hours -ne 1) { 's' })" } + if ($timeSpan.Minutes -gt 0) { $output += "$($timeSpan.Minutes) minute$(if ($timeSpan.Minutes -ne 1) { 's' })" } + if ($timeSpan.Seconds -gt 0) { $output += "$($timeSpan.Seconds) second$(if ($timeSpan.Seconds -ne 1) { 's' })" } + + # Include milliseconds if they exist and are not excluded + if ((($timeSpan.Milliseconds -gt 0) -and !$ExcludeMilliseconds) -or ($output.Count -eq 0)) { + $output += "$($timeSpan.Milliseconds) millisecond$(if ($timeSpan.Milliseconds -ne 1) { 's' })" + } + + return $output -join ', ' + } + } +} + + + +<# +.SYNOPSIS + Converts all instances of 'Start-Sleep' to 'Start-PodeSleep' within a scriptblock. + +.DESCRIPTION + The `ConvertTo-PodeSleep` function processes a given scriptblock and replaces every occurrence + of 'Start-Sleep' with 'Start-PodeSleep'. This is useful for adapting scripts that need to use + Pode-specific sleep functionality. + +.PARAMETER ScriptBlock + The scriptblock to be processed. The function will replace 'Start-Sleep' with 'Start-PodeSleep' + in the provided scriptblock. + +.EXAMPLE + # Example 1: Replace Start-Sleep in a ScriptBlock + $Original = { Write-Host "Starting"; Start-Sleep -Seconds 5; Write-Host "Done" } + $Modified = $Original | ConvertTo-PodeSleep + & $Modified + +.EXAMPLE + # Example 2: Process a ScriptBlock inline + ConvertTo-PodeSleep -ScriptBlock { Start-Sleep -Seconds 2 } | Invoke-Command + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function ConvertTo-PodeSleep { + param( + [Parameter(ValueFromPipeline = $true)] + [scriptblock] + $ScriptBlock + ) + process { + # Modify the ScriptBlock to replace 'Start-Sleep' with 'Start-PodeSleep' + return [scriptblock]::Create(("$($ScriptBlock)" -replace 'Start-Sleep ', 'Start-PodeSleep ')) + } +} + +<# +.SYNOPSIS + Tests whether the current PowerShell host is the Integrated Scripting Environment (ISE). + +.DESCRIPTION + This function checks if the current host is running in the Windows PowerShell ISE + by comparing the `$Host.Name` property with the string 'Windows PowerShell ISE Host'. + +.PARAMETER None + This function does not accept any parameters. + +.OUTPUTS + [Boolean] + Returns `True` if the host is the Windows PowerShell ISE, otherwise `False`. + +.EXAMPLE + Test-PodeIsISEHost + Checks if the current PowerShell session is running in the ISE and returns the result. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeIsISEHost { + return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name)) +} diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index e2c11f81c..394311e4a 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -376,7 +376,11 @@ function Start-PodeLoggingRunspace { $script = { try { - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + + # Check for suspension token 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/OpenApi.ps1 b/src/Private/OpenApi.ps1 index d00073600..2b98a0a45 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -894,7 +894,6 @@ This is an internal function and may change in future releases of Pode. function Get-PodeOpenApiDefinitionInternal { param( - [string] $EndpointName, @@ -1707,6 +1706,7 @@ function Resolve-PodeOAReference { This is an internal function and may change in future releases of Pode. #> function New-PodeOAPropertyInternal { + [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [String] @@ -2371,4 +2371,4 @@ function Test-PodeRouteOADefinitionTag { return $oaDefinitionTag -} \ No newline at end of file +} diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index c954631e5..bb3356d8a 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -24,7 +24,23 @@ function Start-PodeWebServer { $endpoints = @() $endpointsMap = @{} + # Variable to track if a default endpoint is already defined for the current type. + # This ensures that only one default endpoint can be assigned per protocol type (e.g., HTTP, HTTPS). + # If multiple default endpoints are detected, an error will be thrown to prevent configuration issues. + $defaultEndpoint = $false + @(Get-PodeEndpointByProtocolType -Type Http, Ws) | ForEach-Object { + + # Enforce unicity: only one default endpoint is allowed per type. + if ($defaultEndpoint -and $_.Default) { + # A default endpoint for the type '{0}' is already set. Only one default endpoint is allowed per type. Please check your configuration. + throw ($Podelocale.defaultEndpointAlreadySetExceptionMessage -f $($_.Type)) + } + else { + # Assign the current endpoint's Default value for tracking. + $defaultEndpoint = $_.Default + } + # get the ip address $_ip = [string]($_.Address) $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 @@ -52,6 +68,7 @@ function Start-PodeWebServer { Pool = $_.Runspace.PoolName SslProtocols = $_.Ssl.Protocols DualMode = $_.DualMode + Default = $_.Default } # add endpoint to list @@ -68,8 +85,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 +96,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) { @@ -114,173 +144,179 @@ function Start-PodeWebServer { [int] $ThreadId ) + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start + do { + try { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { + # get request and response + $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - # 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) - # add logging endware for post-request - Add-PodeRequestLogEndware -WebEvent $WebEvent + # 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) - # 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 token and wait for the debugger to reset if active + } # 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' = $_ } } } @@ -292,67 +328,74 @@ function Start-PodeWebServer { [Parameter(Mandatory = $true)] $Listener ) + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - $message = (Wait-PodeTask -Task $Listener.GetServerSignalAsync($PodeContext.Tokens.Cancellation.Token)) + do { + try { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { + $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 token and wait for the debugger to reset if active + } Add-PodeRunspace -Type Signals -Name 'Listener' -ScriptBlock $signalScript -Parameters @{ 'Listener' = $listener } @@ -371,74 +414,82 @@ function Start-PodeWebServer { $ThreadId ) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.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 + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start + + do { + try { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { + $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 token and wait for the debugger to reset if active + } # 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' = $_ } } } @@ -451,7 +502,7 @@ function Start-PodeWebServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { Start-Sleep -Seconds 1 } } @@ -486,6 +537,8 @@ function Start-PodeWebServer { Url = $endpoint.Url Pool = $endpoint.Pool DualMode = $endpoint.DualMode + Name = $endpoint.Name + Default = $endpoint.Default } }) } diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index 7bedb2f33..6cf6dee5f 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 { @@ -113,7 +113,7 @@ function Add-PodeRunspace { $null = $ps.AddParameters( @{ 'Type' = $Type - 'Name' = "Pode_$($Type)_$($Name)_$($Id)" + 'Name' = "Pode_$($Type)_$($Name)_$((++$PodeContext.RunspacePools[$Type].LastId))" # create the name and increment the last Id for the type 'NoProfile' = $NoProfile.IsPresent } ) @@ -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 (! $currentRunspace.Name.StartsWith('Pode_')) { + return + } - - - - + # Update the runspace name with the required format + $currentRunspace.Name = "_$($currentRunspace.Name -replace '^(Pode_[^_]+_).+?(_\d+)$', '${1}idle${2}')" +} diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index 924a888d2..dac1c6877 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -50,6 +50,12 @@ function Start-PodeScheduleRunspace { $script = { try { + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start + + # Waits 2 seconds to allow the UI to be visible + Start-Sleep -Seconds 2 + # select the schedules that trigger on-start $_now = [DateTime]::Now @@ -64,9 +70,13 @@ 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 (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + + # Check for suspension token and wait for the debugger to reset if active + Test-PodeSuspensionToken - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { try { $_now = [DateTime]::Now @@ -90,7 +100,7 @@ function Start-PodeScheduleRunspace { Complete-PodeInternalSchedule -Now $_now # cron expression only goes down to the minute, so sleep for 1min - Start-Sleep -Seconds (60 - [DateTime]::Now.Second) + Start-PodeSleep -Seconds (60 - [DateTime]::Now.Second) } catch { $_ | Write-PodeErrorLog @@ -325,6 +335,7 @@ function Get-PodeScheduleScriptBlock { $_ | Write-PodeErrorLog } finally { + Reset-PodeRunspaceName Invoke-PodeGC } } diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 2472c1f91..a87fe1546 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -1,3 +1,33 @@ +<# +.SYNOPSIS + Starts the internal Pode server, initializing configurations, middleware, routes, and runspaces. + +.DESCRIPTION + This function sets up and starts the internal Pode server. It initializes the server's configurations, routes, middleware, runspace pools, logging, and schedules. It also handles different server modes, such as normal, service, or serverless (Azure Functions, AWS Lambda). The function ensures all necessary components are ready and operational before triggering the server's start. + +.PARAMETER Request + Provides request data for serverless execution scenarios. + +.PARAMETER Browse + A switch to enable browsing capabilities for HTTP servers. + +.EXAMPLE + Start-PodeInternalServer + Starts the Pode server in the normal mode with all necessary components initialized. + +.EXAMPLE + Start-PodeInternalServer -Request $RequestData + Starts the Pode server in serverless mode, passing the required request data. + +.EXAMPLE + Start-PodeInternalServer -Browse + Starts the Pode HTTP server with browsing capabilities enabled. + +.NOTES + - This function is used to start the Pode server, either initially or after a restart. + - Handles specific setup for serverless types like Azure Functions and AWS Lambda. + - This is an internal function used within the Pode framework and is subject to change in future releases. +#> function Start-PodeInternalServer { param( [Parameter()] @@ -8,10 +38,14 @@ function Start-PodeInternalServer { ) try { - # Check if the running version of Powershell is EOL - Write-PodeHost "Pode $(Get-PodeVersion) (PID: $($PID))" -ForegroundColor Cyan $null = Test-PodeVersionPwshEOL -ReportUntested + #Show starting console + Show-PodeConsoleInfo -ShowTopSeparator + + # run start event hooks + Invoke-PodeEvent -Type Starting + # setup temp drives for internal dirs Add-PodePSInbuiltDrive @@ -46,7 +80,7 @@ function Start-PodeInternalServer { # load any functions Import-PodeFunctionsIntoRunspaceState -ScriptBlock $_script - # run start event hooks + # run starting event hooks Invoke-PodeEvent -Type Start # start timer for task housekeeping @@ -83,7 +117,7 @@ function Start-PodeInternalServer { } # start the appropriate server - $endpoints = @() + $PodeContext.Server.EndpointsInfo = @() # - service if ($PodeContext.Server.IsService) { @@ -109,121 +143,95 @@ 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)) { - $start = [datetime]::Now - Write-Verbose "Waiting for the $($pool) RunspacePool to be Ready" + if ($PodeContext.Server.EndpointsInfo) { + # Re-order the endpoints + $PodeContext.Server.EndpointsInfo = Get-PodeSortedEndpointsInfo -EndpointsInfo $PodeContext.Server.EndpointsInfo - # wait - while ($PodeContext.RunspacePools[$pool].State -ieq 'Waiting') { - Start-Sleep -Milliseconds 100 - } + # now go back through, and wait for each server type's runspace pool to be ready + foreach ($pool in ($PodeContext.Server.EndpointsInfo.Pool | Sort-Object -Unique)) { + $start = [datetime]::Now + Write-Verbose "Waiting for the $($pool) RunspacePool to be Ready" + + # wait + while ($PodeContext.RunspacePools[$pool].State -ieq 'Waiting') { + Start-Sleep -Milliseconds 100 + } - Write-Verbose "$($pool) RunspacePool $($PodeContext.RunspacePools[$pool].State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]" + Write-Verbose "$($pool) RunspacePool $($PodeContext.RunspacePools[$pool].State) [duration: $(([datetime]::Now - $start).TotalSeconds)s]" - # errored? - if ($PodeContext.RunspacePools[$pool].State -ieq 'error') { - throw ($PodeLocale.runspacePoolFailedToLoadExceptionMessage -f $pool) #"$($pool) RunspacePool failed to load" + # errored? + if ($PodeContext.RunspacePools[$pool].State -ieq 'error') { + throw ($PodeLocale.runspacePoolFailedToLoadExceptionMessage -f $pool) #"$($pool) RunspacePool failed to load" + } } } + else { + Write-Verbose 'No Endpoints defined.' + } } + # Trigger the start + Close-PodeCancellationTokenRequest -Type Start + # set the start time of the server (start and after restart) $PodeContext.Metrics.Server.StartTime = [datetime]::UtcNow # run running event hooks Invoke-PodeEvent -Type Running - # state what endpoints are being listened on - if ($endpoints.Length -gt 0) { + Show-PodeConsoleInfo - # 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 ',')]" - } - - 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 - } - 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 - } - } - 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 - } - } - } - } - } } catch { throw } } +<# +.SYNOPSIS + Restarts the internal Pode server by clearing all configurations, contexts, and states, and reinitializing the server. + +.DESCRIPTION + This function performs a comprehensive restart of the internal Pode server. It resets all contexts, clears caches, schedules, timers, middleware, and security configurations, and reinitializes the server state. It also reloads the server configuration if enabled and increments the server restart count. + +.EXAMPLE + Restart-PodeInternalServer + Restarts the Pode server, clearing all configurations and states before starting it again. +.NOTES + - This function is called internally to restart the Pode server gracefully. + - Handles cancellation tokens, clean-up processes, and reinitialization. + - This is an internal function used within the Pode framework and is subject to change in future releases. +#> function Restart-PodeInternalServer { + + if (!$PodeContext.Tokens.Restart.IsCancellationRequested) { + return + } + try { + Reset-PodeCancellationToken -Type Start # inform restart # Restarting server... - Write-PodeHost $PodeLocale.restartingServerMessage -NoNewline -ForegroundColor Cyan + Show-PodeConsoleInfo - # run restart event hooks - Invoke-PodeEvent -Type Restart + # run restarting event hooks + Invoke-PodeEvent -Type Restarting # cancel the session token - $PodeContext.Tokens.Cancellation.Cancel() + Close-PodeCancellationTokenRequest -Type Cancellation, Terminate # close all current runspaces Close-PodeRunspace -ClosePool @@ -329,21 +337,21 @@ function Restart-PodeInternalServer { $PodeContext.Server.Types = @() # recreate the session tokens - Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation - $PodeContext.Tokens.Cancellation = [System.Threading.CancellationTokenSource]::new() + Reset-PodeCancellationToken -Type Cancellation, Restart, Suspend, Resume, Terminate, Disable - Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart - $PodeContext.Tokens.Restart = [System.Threading.CancellationTokenSource]::new() - - # reload the configuration - $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext - - # done message - Write-PodeHost $PodeLocale.doneMessage -ForegroundColor Green + # if the configuration is enable reload it + if ( $PodeContext.Server.Configuration.Enabled) { + # reload the configuration + $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext -ConfigFile $PodeContext.Server.Configuration.ConfigFile + } # restart the server $PodeContext.Metrics.Server.RestartCount++ + Start-PodeInternalServer + + # run restarting event hooks + Invoke-PodeEvent -Type Restart } catch { $_ | Write-PodeErrorLog @@ -351,6 +359,23 @@ function Restart-PodeInternalServer { } } +<# +.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 is an internal function used within the Pode framework and is subject to change in future releases. +#> function Test-PodeServerKeepOpen { # if we have any timers/schedules/fim - keep open if ((Test-PodeTimersExist) -or (Test-PodeSchedulesExist) -or (Test-PodeFileWatchersExist)) { @@ -364,4 +389,253 @@ function Test-PodeServerKeepOpen { # keep server open return $true +} + +<# +.SYNOPSIS + Suspends the Pode server and its associated 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 suspension status, and provides progress and feedback during the suspension process. + This is primarily used internally by the Pode framework to handle server suspension. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for each runspace to be suspended before timing out. + The default timeout is 30 seconds. + +.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 and is subject to change in future releases. +#> +function Suspend-PodeServerInternal { + param( + [int] + $Timeout = 30 + ) + + # Exit early if no suspension request is pending or if the server is already suspended. + if (!(Test-PodeCancellationTokenRequest -Type Suspend) -or (Test-PodeServerState -State Suspended)) { + return + } + + try { + # Display suspension initiation message in the console. + Show-PodeConsoleInfo + + # Trigger the 'Suspending' event for the server. + Invoke-PodeEvent -Type Suspending + + # Retrieve all Pode-related runspaces for tasks and schedules. + $runspaces = Get-Runspace | Where-Object { $_.Name -like 'Pode_Tasks_*' -or $_.Name -like 'Pode_Schedules_*' } + + # Iterate over each runspace to initiate suspension. + $runspaces | Foreach-Object { + $originalName = $_.Name + $startTime = [DateTime]::UtcNow + $elapsedTime = 0 + + # Activate debug mode on the runspace to suspend it. + Enable-RunspaceDebug -BreakAll -Runspace $_ + + while (! $_.Debugger.InBreakpoint) { + # Calculate elapsed suspension time. + $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds + + # Exit loop if the runspace is already completed. + if ($_.Name.StartsWith('_')) { + Write-Verbose "$originalName runspace has been completed." + break + } + + # Handle timeout scenario and raise an error if exceeded. + if ($elapsedTime -ge $Timeout) { + $errorMsg = "$($_.Name) failed to suspend (Timeout reached after $Timeout seconds)." + Write-PodeHost $errorMsg -ForegroundColor Red + throw $errorMsg + } + + # Pause briefly before rechecking the runspace state. + Start-Sleep -Milliseconds 200 + } + } + } + catch { + # Log any errors encountered during suspension. + $_ | Write-PodeErrorLog + + # Force a resume action to ensure server continuity. + Set-PodeResumeToken + } + finally { + # Reset cancellation token if a cancellation request was made. + if ($PodeContext.Tokens.Cancellation.IsCancellationRequested) { + Reset-PodeCancellationToken -Type Cancellation + } + + # Trigger the 'Suspend' event for the server. + Invoke-PodeEvent -Type Suspend + + # Brief pause before refreshing console output. + Start-Sleep -Seconds 1 + + # Refresh the console and display updated information. + Show-PodeConsoleInfo + } +} + +<# +.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 status, and clears the console for a refreshed view. + The function also provides timeout handling and progress feedback during the resumption process. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for each runspace to exit its suspended state before timing out. + The default timeout is 30 seconds. + +.EXAMPLE + Resume-PodeServerInternal + # Resumes the Pode server after being suspended. + +.NOTES + This is an internal function used within the Pode framework and may change in future releases. +#> +function Resume-PodeServerInternal { + + param( + [int] + $Timeout = 30 + ) + + # Exit early if no resumption request is pending. + if (!(Test-PodeCancellationTokenRequest -Type Resume)) { + return + } + + try { + # Display resumption initiation message in the console. + Show-PodeConsoleInfo + + # Trigger the 'Resuming' event for the server. + Invoke-PodeEvent -Type Resuming + + # Pause briefly to allow processes to stabilize. + Start-Sleep -Seconds 1 + + # Retrieve all runspaces currently in a suspended (debug) state. + $runspaces = Get-Runspace | Where-Object { ($_.Name -like 'Pode_Tasks_*' -or $_.Name -like 'Pode_Schedules_*') -and $_.Debugger.InBreakpoint } + + # Iterate over each suspended runspace to restore normal execution. + $runspaces | ForEach-Object { + # Track the start time for timeout calculations. + $startTime = [DateTime]::UtcNow + $elapsedTime = 0 + + # Disable debug mode on the runspace to resume it. + Disable-RunspaceDebug -Runspace $_ + + while ($_.Debugger.InBreakpoint) { + # Calculate the elapsed time since resumption started. + $elapsedTime = ([DateTime]::UtcNow - $startTime).TotalSeconds + + # Handle timeout scenario and raise an error if exceeded. + if ($elapsedTime -ge $Timeout) { + $errorMsg = "$($_.Name) failed to resume (Timeout reached after $Timeout seconds)." + Write-PodeHost $errorMsg -ForegroundColor Red + throw $errorMsg + } + + # Pause briefly before rechecking the runspace state. + Start-Sleep -Milliseconds 200 + } + } + + # Pause briefly before refreshing the console view. + Start-Sleep -Seconds 1 + } + catch { + # Log any errors encountered during the resumption process. + $_ | Write-PodeErrorLog + + # Force a restart action to recover the server. + Close-PodeCancellationTokenRequest -Type Restart + } + finally { + # Reset the resume cancellation token for future suspension/resumption cycles. + Reset-PodeCancellationToken -Type Resume + + # Trigger the 'Resume' event for the server. + Invoke-PodeEvent -Type Resume + + # Clear the console and display refreshed header information. + Show-PodeConsoleInfo + } +} + +<# +.SYNOPSIS + Enables new requests by removing the middleware that blocks requests when the Pode Watchdog service is active. + +.DESCRIPTION + This function checks if the middleware associated with the Pode Watchdog client is present, and if so, it removes it to allow new requests. + This effectively re-enables access to the service by removing the request blocking. + +.NOTES + This function is used internally to manage Watchdog monitoring and may change in future releases of Pode. +#> +function Enable-PodeServerInternal { + + # Check if the Watchdog middleware exists and remove it if found to allow new requests + if (!(Test-PodeServerState -State Running) -or (Test-PodeServerIsEnabled) ) { + return + } + + # Trigger the 'Enable' event for the server. + Invoke-PodeEvent -Type Enable + + Remove-PodeMiddleware -Name $PodeContext.Server.AllowedActions.DisableSettings.MiddlewareName +} + +<# +.SYNOPSIS + Disables new requests by adding middleware that blocks incoming requests when the Pode Watchdog service is active. + +.DESCRIPTION + This function adds middleware to the Pode server to block new incoming requests while the Pode Watchdog client is active. + It responds to all new requests with a 503 Service Unavailable status and sets a 'Retry-After' header, indicating when the service will be available again. + +.NOTES + This function is used internally to manage Watchdog monitoring and may change in future releases of Pode. +#> +function Disable-PodeServerInternal { + + if (!(Test-PodeServerState -State Running) -or (!( Test-PodeServerIsEnabled)) ) { + return + } + + # Trigger the 'Enable' event for the server. + Invoke-PodeEvent -Type Disable + + # Add middleware to block new requests and respond with 503 Service Unavailable + Add-PodeMiddleware -Name $PodeContext.Server.AllowedActions.DisableSettings.MiddlewareName -ScriptBlock { + # Set HTTP response header for retrying after a certain time (RFC7231) + Set-PodeHeader -Name 'Retry-After' -Value $PodeContext.Server.AllowedActions.DisableSettings.RetryAfter + + # Set HTTP status to 503 Service Unavailable + Set-PodeResponseStatus -Code 503 + + # Stop further processing + return $false + } +} + +function Test-PodeServerIsEnabled { + return !(Test-PodeMiddleware -Name $PodeContext.Server.AllowedActions.DisableSettings.MiddlewareName) } \ No newline at end of file diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index bd7fe6eca..5ea5ff6c4 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 (!(Test-PodeCancellationTokenRequest -Type Terminate)) { # the event object $script:ServiceEvent = @{ Lockable = $PodeContext.Threading.Lockables.Global diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index c3cc7cfd2..93ff26593 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -10,7 +10,24 @@ function Start-PodeSmtpServer { # work out which endpoints to listen on $endpoints = @() + # Variable to track if a default endpoint is already defined for the current type. + # This ensures that only one default endpoint can be assigned per protocol type (e.g., HTTP, HTTPS). + # If multiple default endpoints are detected, an error will be thrown to prevent configuration issues. + $defaultEndpoint = $false + @(Get-PodeEndpointByProtocolType -Type Smtp) | ForEach-Object { + + + # Enforce unicity: only one default endpoint is allowed per type. + if ($defaultEndpoint -and $_.Default) { + # A default endpoint for the type '{0}' is already set. Only one default endpoint is allowed per type. Please check your configuration. + throw ($Podelocale.defaultEndpointAlreadySetExceptionMessage -f $($_.Type)) + } + else { + # Assign the current endpoint's Default value for tracking. + $defaultEndpoint = $_.Default + } + # get the ip address $_ip = [string]($_.Address) $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 @@ -40,6 +57,7 @@ function Start-PodeSmtpServer { Acknowledge = $_.Tcp.Acknowledge SslProtocols = $_.Ssl.Protocols DualMode = $_.DualMode + Default = $_.Default } # add endpoint to list @@ -89,95 +107,103 @@ function Start-PodeSmtpServer { $ThreadId ) - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - # get email - $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start + + do { + try { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { + # 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 token and wait for the debugger to reset if active + } # 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 @@ -189,7 +215,7 @@ function Start-PodeSmtpServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { Start-Sleep -Seconds 1 } } @@ -214,6 +240,8 @@ function Start-PodeSmtpServer { Url = $endpoint.Url Pool = $endpoint.Pool DualMode = $endpoint.DualMode + Name = $endpoint.Name + Default = $endpoint.Default } }) } \ No newline at end of file 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/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 43a3c38c5..84395541e 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -4,7 +4,23 @@ function Start-PodeTcpServer { # work out which endpoints to listen on $endpoints = @() + # Variable to track if a default endpoint is already defined for the current type. + # This ensures that only one default endpoint can be assigned per protocol type (e.g., HTTP, HTTPS). + # If multiple default endpoints are detected, an error will be thrown to prevent configuration issues. + $defaultEndpoint = $false + @(Get-PodeEndpointByProtocolType -Type Tcp) | ForEach-Object { + + # Enforce unicity: only one default endpoint is allowed per type. + if ($defaultEndpoint -and $_.Default) { + # A default endpoint for the type '{0}' is already set. Only one default endpoint is allowed per type. Please check your configuration. + throw ($Podelocale.defaultEndpointAlreadySetExceptionMessage -f $($_.Type)) + } + else { + # Assign the current endpoint's Default value for tracking. + $defaultEndpoint = $_.Default + } + # get the ip address $_ip = [string]($_.Address) $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 @@ -35,6 +51,7 @@ function Start-PodeTcpServer { CRLFMessageEnd = $_.Tcp.CRLFMessageEnd SslProtocols = $_.Ssl.Protocols DualMode = $_.DualMode + Default = $_.Default } # add endpoint to list @@ -84,118 +101,125 @@ function Start-PodeTcpServer { [int] $ThreadId ) + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start - try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - # get email - $context = (Wait-PodeTask -Task $Listener.GetContextAsync($PodeContext.Tokens.Cancellation.Token)) + do { + try { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { + # 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 token and wait for the debugger to reset if active + } # 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 @@ -207,7 +231,7 @@ function Start-PodeTcpServer { ) try { - while ($Listener.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Listener.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { Start-Sleep -Seconds 1 } } @@ -232,6 +256,8 @@ function Start-PodeTcpServer { Url = $endpoint.Url Pool = $endpoint.Pool DualMode = $endpoint.DualMode + Name = $endpoint.Name + Default = $endpoint.Default } }) } diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index 48b19ba9a..eac07e8fc 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -19,14 +19,22 @@ function Start-PodeTimerRunspace { } $script = { - try { + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + try { + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + # Check for suspension token 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 token 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 b1c0cdc8f..cb7c92928 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -50,64 +50,71 @@ function Start-PodeWebSocketRunspace { [int] $ThreadId ) + # Waits for the Pode server to fully start before proceeding with further operations. + Wait-PodeCancellationTokenRequest -Type Start - try { - while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - # get request - $request = (Wait-PodeTask -Task $Receiver.GetWebSocketRequestAsync($PodeContext.Tokens.Cancellation.Token)) + do { + try { + while ($Receiver.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { + # 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 token and wait for the debugger to reset if active + } # 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 @@ -119,9 +126,11 @@ function Start-PodeWebSocketRunspace { ) try { - while ($Receiver.IsConnected -and !$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while ($Receiver.IsConnected -and !(Test-PodeCancellationTokenRequest -Type Terminate)) { Start-Sleep -Seconds 1 } + + } catch [System.OperationCanceledException] { $_ | Write-PodeErrorLog -Level Debug diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 3d609f763..047fa891b 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -1,78 +1,121 @@ <# .SYNOPSIS -Starts a Pode Server with the supplied ScriptBlock. + Starts a Pode server with the supplied script block or file containing the server logic. .DESCRIPTION -Starts a Pode Server with the supplied ScriptBlock. + This function initializes and starts a Pode server based on the provided configuration. + It supports both inline script blocks and external files for defining server logic. + The server's behavior, console output, and various features can be customized using parameters. + Additionally, it manages server termination, cancellation, and cleanup processes. .PARAMETER ScriptBlock -The main logic for the Server. + The main logic for the server, provided as a script block. .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 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. + Specifies the interval in seconds for invoking the script block in 'Service' type servers. .PARAMETER Name -An optional name for the Server (intended for future ideas). + An optional name for the server, useful for identification in logs and future extensions. .PARAMETER Threads -The numbers of threads to use for Web, SMTP, and TCP servers. + The number of threads to allocate for Web, SMTP, and TCP servers. Defaults to 1. .PARAMETER RootPath -An override for the Server's root path. + Overrides the server's root path. If not provided, the root path will be derived from the file path or the current working directory. .PARAMETER Request -Intended for Serverless environments, this is Requests details that Pode can parse and use. + Provides request details for serverless environments 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. + Specifies the serverless type for Pode. Valid values are: + - AzureFunctions + - AwsLambda .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. + Controls the visibility of stack traces on status pages. Valid values are: + - Show + - Hide .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. + Specifies a custom socket listener. Defaults to Pode's inbuilt listener. + +.PARAMETER EnablePool + Configures specific runspace pools (e.g., Timers, Schedules, Tasks, WebSockets, Files) for ad-hoc usage. + +.PARAMETER Browse + Opens the default web endpoint in the browser upon server start. + +.PARAMETER CurrentPath + Sets the server's root path to the current working directory. Only applicable when -FilePath is used. + +.PARAMETER EnableBreakpoints + Enables breakpoints created using `Wait-PodeDebugger`. .PARAMETER DisableTermination -Disables the ability to terminate the Server. + Prevents termination, suspension, or resumption of the server via console commands. + +.PARAMETER DisableConsoleInput + Disables all console interactions for the server. + +.PARAMETER ClearHost + Clears the console screen whenever the server state changes (e.g., running → suspend → resume). .PARAMETER Quiet -Disables any output from the Server. + Suppresses all output from the server. -.PARAMETER Browse -Open the web Server's default endpoint in your default browser. +.PARAMETER HideOpenAPI + Hides OpenAPI details such as specification and documentation URLs from the console output. -.PARAMETER CurrentPath -Sets the Server's root path to be the current working path - for -FilePath only. +.PARAMETER HideEndpoints + Hides the list of active endpoints from the console output. -.PARAMETER EnablePool -Tells Pode to configure certain RunspacePools when they're being used adhoc, such as Timers or Schedules. +.PARAMETER ShowHelp + Displays a help menu in the console with available control commands. -.PARAMETER EnableBreakpoints -If supplied, any breakpoints created by using Wait-PodeDebugger will be enabled - or disabled if false passed explicitly, or not supplied. +.PARAMETER IgnoreServerConfig + Prevents the server from loading settings from the server.psd1 configuration file. + +.PARAMETER ConfigFile + Specifies a custom configuration file instead of using the default `server.psd1`. + +.PARAMETER Daemon + Configures the server to run as a daemon with minimal console interaction and output. .EXAMPLE -Start-PodeServer { /* logic */ } + Start-PodeServer { /* server logic */ } + Starts a Pode server using the supplied script block. .EXAMPLE -Start-PodeServer -Interval 10 { /* logic */ } + Start-PodeServer -FilePath './server.ps1' -Browse + Starts a Pode server using the logic defined in an external file and opens the default endpoint in the browser. .EXAMPLE -Start-PodeServer -Request $LambdaInput -ServerlessType AwsLambda { /* logic */ } + Start-PodeServer -ServerlessType AwsLambda -Request $LambdaInput { /* server logic */ } + Starts a Pode server in a serverless environment, using AWS Lambda input. + +.EXAMPLE + Start-PodeServer -HideOpenAPI -ClearHost { /* server logic */ } + Starts a Pode server with console output configured to hide OpenAPI details and clear the console on state changes. + +.NOTES + This function is part of the Pode framework and is responsible for server initialization, configuration, + request handling, and cleanup. It supports both standalone and serverless deployments, and provides + extensive customization options for developers. #> function Start-PodeServer { [CmdletBinding(DefaultParameterSetName = 'Script')] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Script')] + [Parameter(Mandatory = $true, ParameterSetName = 'ScriptDaemon')] [scriptblock] $ScriptBlock, [Parameter(Mandatory = $true, ParameterSetName = 'File')] + [Parameter(Mandatory = $true, ParameterSetName = 'FileDaemon')] [string] $FilePath, @@ -114,22 +157,60 @@ function Start-PodeServer { [string[]] $EnablePool, + [Parameter(ParameterSetName = 'File')] + [Parameter(ParameterSetName = 'Script')] + [switch] + $Browse, + + [Parameter(Mandatory = $true, ParameterSetName = 'FileDaemon')] + [Parameter(ParameterSetName = 'File')] + [switch] + $CurrentPath, + + [Parameter(ParameterSetName = 'File')] + [Parameter(ParameterSetName = 'Script')] + [switch] + $EnableBreakpoints, + + [Parameter(ParameterSetName = 'File')] + [Parameter(ParameterSetName = 'Script')] [switch] $DisableTermination, + [Parameter(ParameterSetName = 'File')] + [Parameter(ParameterSetName = 'Script')] [switch] $Quiet, + [Parameter(ParameterSetName = 'File')] + [Parameter(ParameterSetName = 'Script')] [switch] - $Browse, + $DisableConsoleInput, - [Parameter(ParameterSetName = 'File')] [switch] - $CurrentPath, + $ClearHost, + + [switch] + $HideOpenAPI, + + [switch] + $HideEndpoints, + + [switch] + $ShowHelp, [switch] - $EnableBreakpoints + $IgnoreServerConfig, + + [string] + $ConfigFile, + + [Parameter(Mandatory = $true, ParameterSetName = 'FileDaemon')] + [Parameter(Mandatory = $true, ParameterSetName = 'ScriptDaemon')] + [switch] + $Daemon ) + begin { $pipelineItemCount = 0 } @@ -147,7 +228,7 @@ function Start-PodeServer { Set-PodeCurrentRunspaceName -Name 'PodeServer' # ensure the session is clean - $PodeContext = $null + $Script:PodeContext = $null $ShowDoneMessage = $true try { @@ -171,26 +252,43 @@ 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 - - # set it so ctrl-c can terminate, unless serverless/iis, or disabled - if (!$PodeContext.Server.DisableTermination -and ($null -eq $psISE)) { - [Console]::TreatControlCAsInput = $true + + # 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 = Get-PodeDefaultConsole + EnableBreakpoints = $EnableBreakpoints + IgnoreServerConfig = $IgnoreServerConfig + ConfigFile = $ConfigFile + } + + + # Create main context object + $PodeContext = New-PodeContext @ContextParams + + # Define parameter values with comments explaining each one + $ConfigParameters = @{ + DisableTermination = $DisableTermination # Disable termination of the Pode server from the console + DisableConsoleInput = $DisableConsoleInput # Disable input from the console for the Pode server + Quiet = $Quiet # Enable quiet mode, suppressing console output + ClearHost = $ClearHost # Clear the host on startup + HideOpenAPI = $HideOpenAPI # Hide the OpenAPI documentation display + HideEndpoints = $HideEndpoints # Hide the endpoints list display + ShowHelp = $ShowHelp # Show help information in the console + Daemon = $Daemon # Enable daemon mode, combining multiple configurations } + # Call the function using splatting + Set-PodeConsoleOverrideConfiguration @ConfigParameters + # start the file monitor for interally restarting Start-PodeFileMonitor @@ -202,36 +300,38 @@ function Start-PodeServer { return } - # 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 - - # get the next key presses - $key = Get-PodeConsoleKey + # Sit in a loop waiting for server termination/cancellation or a restart request. + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + # Retrieve the current state of the server (e.g., Running, Suspended). + $serverState = Get-PodeServerState - # check for internal restart - if (($PodeContext.Tokens.Restart.IsCancellationRequested) -or (Test-PodeRestartPressed -Key $key)) { - Restart-PodeInternalServer + # If console input is not disabled, invoke any actions based on console commands. + if (!$PodeContext.Server.Console.DisableConsoleInput) { + Invoke-PodeConsoleAction -ServerState $serverState } - # check for open browser - if (Test-PodeOpenBrowserPressed -Key $key) { - Invoke-PodeEvent -Type Browser - Start-Process (Get-PodeEndpointUrl) - } + # Resolve cancellation token requests (e.g., Restart, Enable/Disable, Suspend/Resume). + Resolve-PodeCancellationToken -ServerState $serverState + + # Pause for 1 second before re-checking the state and processing the next action. + Start-Sleep -Seconds 1 } + if ($PodeContext.Server.IsIIS -and $PodeContext.Server.IIS.Shutdown) { # (IIS Shutdown) Write-PodeHost $PodeLocale.iisShutdownMessage -NoNewLine -ForegroundColor Yellow Write-PodeHost ' ' -NoNewLine } + # Terminating... - Write-PodeHost $PodeLocale.terminatingMessage -NoNewLine -ForegroundColor Yellow Invoke-PodeEvent -Type Terminate - $PodeContext.Tokens.Cancellation.Cancel() + Close-PodeServer + Show-PodeConsoleInfo } catch { + $_ | Write-PodeErrorLog + Invoke-PodeEvent -Type Crash $ShowDoneMessage = $false throw @@ -246,32 +346,39 @@ function Start-PodeServer { Unregister-PodeSecretVaultsInternal # clean the runspaces and tokens - Close-PodeServerInternal -ShowDoneMessage:$ShowDoneMessage + Close-PodeServerInternal - # clean the session - $PodeContext = $null + Show-PodeConsoleInfo # Restore the name of the current runspace Set-PodeCurrentRunspaceName -Name $previousRunspaceName + + if (($ShowDoneMessage -and ($PodeContext.Server.Types.Length -gt 0) -and !$PodeContext.Server.IsServerless)) { + Write-PodeHost $PodeLocale.doneMessage -ForegroundColor Green + } + + # clean the session + $PodeContext = $null + $PodeLocale = $null } } } <# .SYNOPSIS -Closes the Pode server. + Closes the Pode server. .DESCRIPTION -Closes the Pode server. + Closes the Pode server. .EXAMPLE -Close-PodeServer + Close-PodeServer #> function Close-PodeServer { [CmdletBinding()] param() - $PodeContext.Tokens.Cancellation.Cancel() + Close-PodeCancellationTokenRequest -Type Cancellation, Terminate } <# @@ -288,7 +395,79 @@ function Restart-PodeServer { [CmdletBinding()] param() - $PodeContext.Tokens.Restart.Cancel() + # Only if the Restart feature is anabled + if ($PodeContext.Server.AllowedActions.Restart) { + Close-PodeCancellationTokenRequest -Type Restart + } +} + + +<# +.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. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for each runspace to be recovered before timing out. Default is 30 seconds. + +.EXAMPLE + Resume-PodeServer + # Resumes the Pode server after a suspension. + +#> +function Resume-PodeServer { + [CmdletBinding()] + param( + [int] + $Timeout + ) + # Only if the Suspend feature is anabled + if ($PodeContext.Server.AllowedActions.Suspend) { + if ($Timeout) { + $PodeContext.Server.AllowedActions.Timeout.Resume = $Timeout + } + + if ((Test-PodeServerState -State Suspended)) { + Set-PodeResumeToken + } + } +} + + +<# +.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-PodeServer + # Suspends the Pode server with a timeout of 60 seconds. + +#> +function Suspend-PodeServer { + [CmdletBinding()] + param( + [int] + $Timeout + ) + # Only if the Suspend feature is anabled + if ($PodeContext.Server.AllowedActions.Suspend) { + if ($Timeout) { + $PodeContext.Server.AllowedActions.Timeout.Suspend = $Timeout + } + if (!(Test-PodeServerState -State Suspended)) { + Set-PodeSuspendToken + } + } } <# @@ -723,594 +902,6 @@ function Show-PodeGui { } } -<# -.SYNOPSIS -Bind an endpoint to listen for incoming Requests. - -.DESCRIPTION -Bind an endpoint to listen for incoming Requests. The endpoints can be HTTP, HTTPS, TCP or SMTP, with the option to bind certificates. - -.PARAMETER Address -The IP/Hostname of the endpoint (Default: localhost). - -.PARAMETER Port -The Port number of the endpoint. - -.PARAMETER Hostname -An optional hostname for the endpoint, specifying a hostname restricts access to just the hostname. - -.PARAMETER Protocol -The protocol of the supplied endpoint. - -.PARAMETER Certificate -The path to a certificate that can be use to enable HTTPS - -.PARAMETER CertificatePassword -The password for the certificate file referenced in Certificate - -.PARAMETER CertificateKey -A key file to be paired with a PEM certificate file referenced in Certificate - -.PARAMETER CertificateThumbprint -A certificate thumbprint to bind onto HTTPS endpoints (Windows). - -.PARAMETER CertificateName -A certificate subject name to bind onto HTTPS endpoints (Windows). - -.PARAMETER CertificateStoreName -The name of a certifcate store where a certificate can be found (Default: My) (Windows). - -.PARAMETER CertificateStoreLocation -The location of a certifcate store where a certificate can be found (Default: CurrentUser) (Windows). - -.PARAMETER X509Certificate -The raw X509 certificate that can be use to enable HTTPS - -.PARAMETER TlsMode -The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit). - -.PARAMETER Name -An optional name for the endpoint, that can be used with other functions (Default: GUID). - -.PARAMETER RedirectTo -The Name of another Endpoint to automatically generate a redirect route for all traffic. - -.PARAMETER Description -A quick description of the Endpoint - normally used in OpenAPI. - -.PARAMETER Acknowledge -An optional Acknowledge message to send to clients when they first connect, for TCP and SMTP endpoints only. - -.PARAMETER SslProtocol -One or more optional SSL Protocols this endpoints supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS). - -.PARAMETER CRLFMessageEnd -If supplied, TCP endpoints will expect incoming data to end with CRLF. - -.PARAMETER Force -Ignore Adminstrator checks for non-localhost endpoints. - -.PARAMETER SelfSigned -Create and bind a self-signed certifcate for HTTPS endpoints. - -.PARAMETER AllowClientCertificate -Allow for client certificates to be sent on requests. - -.PARAMETER PassThru -If supplied, the endpoint created will be returned. - -.PARAMETER LookupHostname -If supplied, a supplied Hostname will have its IP Address looked up from host file or DNS. - -.PARAMETER DualMode -If supplied, this endpoint will listen on both the IPv4 and IPv6 versions of the supplied -Address. -For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 address. - -.PARAMETER Default -If supplied, this endpoint will be the default one used for internally generating URLs. - -.EXAMPLE -Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http - -.EXAMPLE -Add-PodeEndpoint -Address localhost -Protocol Smtp - -.EXAMPLE -Add-PodeEndpoint -Address dev.pode.com -Port 8443 -Protocol Https -SelfSigned - -.EXAMPLE -Add-PodeEndpoint -Address 127.0.0.2 -Hostname dev.pode.com -Port 8443 -Protocol Https -SelfSigned - -.EXAMPLE -Add-PodeEndpoint -Address live.pode.com -Protocol Https -CertificateThumbprint '2A9467F7D3940243D6C07DE61E7FCCE292' -#> -function Add-PodeEndpoint { - [CmdletBinding(DefaultParameterSetName = 'Default')] - [OutputType([hashtable])] - param( - [Parameter()] - [string] - $Address = 'localhost', - - [Parameter()] - [int] - $Port = 0, - - [Parameter()] - [string] - $Hostname, - - [Parameter()] - [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')] - [string] - $Protocol, - - [Parameter(Mandatory = $true, ParameterSetName = 'CertFile')] - [string] - $Certificate = $null, - - [Parameter(ParameterSetName = 'CertFile')] - [string] - $CertificatePassword = $null, - - [Parameter(ParameterSetName = 'CertFile')] - [string] - $CertificateKey = $null, - - [Parameter(Mandatory = $true, ParameterSetName = 'CertThumb')] - [string] - $CertificateThumbprint, - - [Parameter(Mandatory = $true, ParameterSetName = 'CertName')] - [string] - $CertificateName, - - [Parameter(ParameterSetName = 'CertName')] - [Parameter(ParameterSetName = 'CertThumb')] - [System.Security.Cryptography.X509Certificates.StoreName] - $CertificateStoreName = 'My', - - [Parameter(ParameterSetName = 'CertName')] - [Parameter(ParameterSetName = 'CertThumb')] - [System.Security.Cryptography.X509Certificates.StoreLocation] - $CertificateStoreLocation = 'CurrentUser', - - [Parameter(Mandatory = $true, ParameterSetName = 'CertRaw')] - [X509Certificate] - $X509Certificate = $null, - - [Parameter(ParameterSetName = 'CertFile')] - [Parameter(ParameterSetName = 'CertThumb')] - [Parameter(ParameterSetName = 'CertName')] - [Parameter(ParameterSetName = 'CertRaw')] - [Parameter(ParameterSetName = 'CertSelf')] - [ValidateSet('Implicit', 'Explicit')] - [string] - $TlsMode = 'Implicit', - - [Parameter()] - [string] - $Name = $null, - - [Parameter()] - [string] - $RedirectTo = $null, - - [Parameter()] - [string] - $Description, - - [Parameter()] - [string] - $Acknowledge, - - [Parameter()] - [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')] - [string[]] - $SslProtocol = $null, - - [switch] - $CRLFMessageEnd, - - [switch] - $Force, - - [Parameter(ParameterSetName = 'CertSelf')] - [switch] - $SelfSigned, - - [switch] - $AllowClientCertificate, - - [switch] - $PassThru, - - [switch] - $LookupHostname, - - [switch] - $DualMode, - - [switch] - $Default - ) - - # error if serverless - Test-PodeIsServerless -FunctionName 'Add-PodeEndpoint' -ThrowError - - # if RedirectTo is supplied, then a Name is mandatory - if (![string]::IsNullOrWhiteSpace($RedirectTo) -and [string]::IsNullOrWhiteSpace($Name)) { - # A Name is required for the endpoint if the RedirectTo parameter is supplied - throw ($PodeLocale.nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage) - } - - # get the type of endpoint - $type = Get-PodeEndpointType -Protocol $Protocol - - # are we running as IIS for HTTP/HTTPS? (if yes, force the port, address and protocol) - $isIIS = ((Test-PodeIsIIS) -and (@('Http', 'Ws') -icontains $type)) - if ($isIIS) { - $Port = [int]$env:ASPNETCORE_PORT - $Address = '127.0.0.1' - $Hostname = [string]::Empty - $Protocol = $type - } - - # are we running as Heroku for HTTP/HTTPS? (if yes, force the port, address and protocol) - $isHeroku = ((Test-PodeIsHeroku) -and (@('Http') -icontains $type)) - if ($isHeroku) { - $Port = [int]$env:PORT - $Address = '0.0.0.0' - $Hostname = [string]::Empty - $Protocol = $type - } - - # parse the endpoint for host/port info - if (![string]::IsNullOrWhiteSpace($Hostname) -and !(Test-PodeHostname -Hostname $Hostname)) { - # Invalid hostname supplied - throw ($PodeLocale.invalidHostnameSuppliedExceptionMessage -f $Hostname) - } - - if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) { - $Hostname = $Address - $Address = 'localhost' - } - - if (![string]::IsNullOrWhiteSpace($Hostname) -and $LookupHostname) { - $Address = (Get-PodeIPAddressesForHostname -Hostname $Hostname -Type All | Select-Object -First 1) - } - - $_endpoint = Get-PodeEndpointInfo -Address "$($Address):$($Port)" - - # if no name, set to guid, then check uniqueness - if ([string]::IsNullOrWhiteSpace($Name)) { - $Name = New-PodeGuid -Secure - } - - if ($PodeContext.Server.Endpoints.ContainsKey($Name)) { - # An endpoint named has already been defined - throw ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f $Name) - } - - # protocol must be https for client certs, or hosted behind a proxy like iis - if (($Protocol -ine 'https') -and !(Test-PodeIsHosted) -and $AllowClientCertificate) { - # Client certificates are only supported on HTTPS endpoints - throw ($PodeLocale.clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage) - } - - # explicit tls is only supported for smtp/tcp - if (($type -inotin @('smtp', 'tcp')) -and ($TlsMode -ieq 'explicit')) { - # The Explicit TLS mode is only supported on SMTPS and TCPS endpoints - throw ($PodeLocale.explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage) - } - - # ack message is only for smtp/tcp - if (($type -inotin @('smtp', 'tcp')) -and ![string]::IsNullOrEmpty($Acknowledge)) { - # The Acknowledge message is only supported on SMTP and TCP endpoints - throw ($PodeLocale.acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage) - } - - # crlf message end is only for tcp - if (($type -ine 'tcp') -and $CRLFMessageEnd) { - # The CRLF message end check is only supported on TCP endpoints - throw ($PodeLocale.crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage) - } - - # new endpoint object - $obj = @{ - Name = $Name - Description = $Description - DualMode = $DualMode - Address = $null - RawAddress = $null - Port = $null - IsIPAddress = $true - HostName = $Hostname - FriendlyName = $Hostname - Url = $null - Ssl = @{ - Enabled = (@('https', 'wss', 'smtps', 'tcps') -icontains $Protocol) - Protocols = $PodeContext.Server.Sockets.Ssl.Protocols - } - Protocol = $Protocol.ToLowerInvariant() - Type = $type.ToLowerInvariant() - Runspace = @{ - PoolName = (Get-PodeEndpointRunspacePoolName -Protocol $Protocol) - } - Default = $Default.IsPresent - Certificate = @{ - Raw = $X509Certificate - SelfSigned = $SelfSigned - AllowClientCertificate = $AllowClientCertificate - TlsMode = $TlsMode - } - Tcp = @{ - Acknowledge = $Acknowledge - CRLFMessageEnd = $CRLFMessageEnd - } - } - - # set ssl protocols - if (!(Test-PodeIsEmpty $SslProtocol)) { - $obj.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $SslProtocol) - } - - # set the ip for the context (force to localhost for IIS) - $obj.Address = Get-PodeIPAddress $_endpoint.Host -DualMode:$DualMode - $obj.IsIPAddress = [string]::IsNullOrWhiteSpace($obj.HostName) - - if ($obj.IsIPAddress) { - if (!(Test-PodeIPAddressLocalOrAny -IP $obj.Address)) { - $obj.FriendlyName = "$($obj.Address)" - } - else { - $obj.FriendlyName = 'localhost' - } - } - - # set the port for the context, if 0 use a default port for protocol - $obj.Port = $_endpoint.Port - if (([int]$obj.Port) -eq 0) { - $obj.Port = Get-PodeDefaultPort -Protocol $Protocol -TlsMode $TlsMode - } - - if ($obj.IsIPAddress) { - $obj.RawAddress = "$($obj.Address):$($obj.Port)" - } - else { - $obj.RawAddress = "$($obj.FriendlyName):$($obj.Port)" - } - - # set the url of this endpoint - $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 - throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) - } - - # has this endpoint been added before? (for http/https we can just not add it again) - $exists = ($PodeContext.Server.Endpoints.Values | Where-Object { - ($_.FriendlyName -ieq $obj.FriendlyName) -and ($_.Port -eq $obj.Port) -and ($_.Ssl.Enabled -eq $obj.Ssl.Enabled) -and ($_.Type -ieq $obj.Type) - } | Measure-Object).Count - - # if we're dealing with a certificate, attempt to import it - if (!(Test-PodeIsHosted) -and ($PSCmdlet.ParameterSetName -ilike 'cert*')) { - # fail if protocol is not https - if (@('https', 'wss', 'smtps', 'tcps') -inotcontains $Protocol) { - # Certificate supplied for non-HTTPS/WSS endpoint - throw ($PodeLocale.certificateSuppliedForNonHttpsWssEndpointExceptionMessage) - } - - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'certfile' { - $obj.Certificate.Raw = Get-PodeCertificateByFile -Certificate $Certificate -Password $CertificatePassword -Key $CertificateKey - } - - 'certthumb' { - $obj.Certificate.Raw = Get-PodeCertificateByThumbprint -Thumbprint $CertificateThumbprint -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation - } - - 'certname' { - $obj.Certificate.Raw = Get-PodeCertificateByName -Name $CertificateName -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation - } - - 'certself' { - $obj.Certificate.Raw = New-PodeSelfSignedCertificate - } - } - - # fail if the cert is expired - if ($obj.Certificate.Raw.NotAfter -lt [datetime]::Now) { - # The certificate has expired - throw ($PodeLocale.certificateExpiredExceptionMessage -f $obj.Certificate.Raw.Subject, $obj.Certificate.Raw.NotAfter) - } - } - - if (!$exists) { - # set server type - $_type = $type - if ($_type -iin @('http', 'ws')) { - $_type = 'http' - } - - if ($PodeContext.Server.Types -inotcontains $_type) { - $PodeContext.Server.Types += $_type - } - - # add the new endpoint - $PodeContext.Server.Endpoints[$Name] = $obj - $PodeContext.Server.EndpointsMap["$($obj.Protocol)|$($obj.RawAddress)"] = $Name - } - - # if RedirectTo is set, attempt to build a redirecting route - if (!(Test-PodeIsHosted) -and ![string]::IsNullOrWhiteSpace($RedirectTo)) { - $redir_endpoint = $PodeContext.Server.Endpoints[$RedirectTo] - - # ensure the name exists - if (Test-PodeIsEmpty $redir_endpoint) { - # An endpoint named has not been defined for redirecting - throw ($PodeLocale.endpointNotDefinedForRedirectingExceptionMessage -f $RedirectTo) - } - - # build the redirect route - Add-PodeRoute -Method * -Path * -EndpointName $obj.Name -ArgumentList $redir_endpoint -ScriptBlock { - param($endpoint) - Move-PodeResponseUrl -EndpointName $endpoint.Name - } - } - - # return the endpoint? - if ($PassThru) { - return $obj - } -} - -<# -.SYNOPSIS -Get an Endpoint(s). - -.DESCRIPTION -Get an Endpoint(s). - -.PARAMETER Address -An Address to filter the endpoints. - -.PARAMETER Port -A Port to filter the endpoints. - -.PARAMETER Hostname -A Hostname to filter the endpoints. - -.PARAMETER Protocol -A Protocol to filter the endpoints. - -.PARAMETER Name -Any endpoints Names to filter endpoints. - -.EXAMPLE -Get-PodeEndpoint -Address 127.0.0.1 - -.EXAMPLE -Get-PodeEndpoint -Protocol Http - -.EXAMPLE -Get-PodeEndpoint -Name Admin, User -#> -function Get-PodeEndpoint { - [CmdletBinding()] - param( - [Parameter()] - [string] - $Address, - - [Parameter()] - [int] - $Port = 0, - - [Parameter()] - [string] - $Hostname, - - [Parameter()] - [ValidateSet('', 'Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')] - [string] - $Protocol, - - [Parameter()] - [string[]] - $Name - ) - - if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) { - $Hostname = $Address - $Address = 'localhost' - } - - $endpoints = $PodeContext.Server.Endpoints.Values - - # if we have an address, filter - if (![string]::IsNullOrWhiteSpace($Address)) { - if (($Address -eq '*') -or $PodeContext.Server.IsHeroku) { - $Address = '0.0.0.0' - } - - if ($PodeContext.Server.IsIIS -or ($Address -ieq 'localhost')) { - $Address = '127.0.0.1' - } - - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Address.ToString() -ine $Address) { - continue - } - - $endpoint - }) - } - - # if we have a hostname, filter - if (![string]::IsNullOrWhiteSpace($Hostname)) { - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Hostname.ToString() -ine $Hostname) { - continue - } - - $endpoint - }) - } - - # if we have a port, filter - if ($Port -gt 0) { - if ($PodeContext.Server.IsIIS) { - $Port = [int]$env:ASPNETCORE_PORT - } - - if ($PodeContext.Server.IsHeroku) { - $Port = [int]$env:PORT - } - - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Port -ne $Port) { - continue - } - - $endpoint - }) - } - - # if we have a protocol, filter - if (![string]::IsNullOrWhiteSpace($Protocol)) { - if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) { - $Protocol = 'Http' - } - - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Protocol -ine $Protocol) { - continue - } - - $endpoint - }) - } - - # further filter by endpoint names - if (($null -ne $Name) -and ($Name.Length -gt 0)) { - $endpoints = @(foreach ($_name in $Name) { - foreach ($endpoint in $endpoints) { - if ($endpoint.Name -ine $_name) { - continue - } - - $endpoint - } - }) - } - - # return - return $endpoints -} - <# .SYNOPSIS Sets the path for a specified default folder type in the Pode server context. @@ -1402,13 +993,13 @@ function Get-PodeDefaultFolder { <# .SYNOPSIS -Attaches a breakpoint which can be used for debugging. + Attaches a breakpoint which can be used for debugging. .DESCRIPTION -Attaches a breakpoint which can be used for debugging. + Attaches a breakpoint which can be used for debugging. .EXAMPLE -Wait-PodeDebugger + Wait-PodeDebugger #> function Wait-PodeDebugger { [CmdletBinding()] @@ -1419,4 +1010,150 @@ function Wait-PodeDebugger { } Wait-Debugger -} \ No newline at end of file +} + + +<# +.SYNOPSIS + Retrieves the current state of the Pode server. + +.DESCRIPTION + The Get-PodeServerState function evaluates the internal state of the Pode server based on the cancellation tokens available + in the $PodeContext. The function determines if the server is running, terminating, restarting, suspending, resuming, or + in any other predefined state. + +.OUTPUTS + [string] - The state of the Pode server as one of the following values: + 'Terminated', 'Terminating', 'Resuming', 'Suspending', 'Suspended', 'Restarting', 'Starting', 'Running'. + +.EXAMPLE + Get-PodeServerState + + Retrieves the current state of the Pode server and returns it as a string. +#> +function Get-PodeServerState { + [CmdletBinding()] + [OutputType([Pode.PodeServerState])] + param() + # Check if PodeContext or its Tokens property is null; if so, consider the server terminated + if ($null -eq $PodeContext -or $null -eq $PodeContext.Tokens) { + return [Pode.PodeServerState]::Terminated + } + + # Check if the server is in the process of terminating + if (Test-PodeCancellationTokenRequest -Type Terminate) { + return [Pode.PodeServerState]::Terminating + } + + # Check if the server is resuming from a suspended state + if (Test-PodeCancellationTokenRequest -Type Resume) { + return [Pode.PodeServerState]::Resuming + } + + # Check if the server is in the process of restarting + if (Test-PodeCancellationTokenRequest -Type Restart) { + return [Pode.PodeServerState]::Restarting + } + + # Check if the server is suspending or already suspended + if (Test-PodeCancellationTokenRequest -Type Suspend) { + if (Test-PodeCancellationTokenRequest -Type Cancellation) { + return [Pode.PodeServerState]::Suspending + } + return [Pode.PodeServerState]::Suspended + } + + # Check if the server is starting + if (!(Test-PodeCancellationTokenRequest -Type Start)) { + return [Pode.PodeServerState]::Starting + } + + # If none of the above, assume the server is running + return [Pode.PodeServerState]::Running +} + +<# +.SYNOPSIS + Tests whether the Pode server is in a specified state. + +.DESCRIPTION + The `Test-PodeServerState` function checks the current state of the Pode server + by calling `Get-PodeServerState` and comparing the result to the specified state. + The function returns `$true` if the server is in the specified state and `$false` otherwise. + +.PARAMETER State + Specifies the server state to test. Allowed values are: + - `Terminated`: The server is not running, and the context is null. + - `Terminating`: The server is in the process of shutting down. + - `Resuming`: The server is resuming from a suspended state. + - `Suspending`: The server is in the process of entering a suspended state. + - `Suspended`: The server is fully suspended. + - `Restarting`: The server is restarting. + - `Starting`: The server is in the process of starting up. + - `Running`: The server is actively running. + +.EXAMPLE + Test-PodeServerState -State 'Running' + + Returns `$true` if the server is currently running, otherwise `$false`. + +.EXAMPLE + Test-PodeServerState -State 'Suspended' + + Returns `$true` if the server is fully suspended, otherwise `$false`. + +.NOTES + This function is part of Pode's server state management utilities. + It relies on the `Get-PodeServerState` function to determine the current state. +#> +function Test-PodeServerState { + param( + [Parameter(Mandatory = $true)] + [Pode.PodeServerState] + $State + ) + + # Call Get-PodeServerState to retrieve the current server state + $currentState = Get-PodeServerState + + # Return true if the current state matches the provided state, otherwise false + return $currentState -eq $State +} + +<# +.SYNOPSIS + Enables new incoming requests by removing the middleware that blocks requests when the Pode Watchdog client is active. + +.DESCRIPTION + This function resets the cancellation token for the Disable action, allowing the Pode server to accept new incoming requests. +#> +function Enable-PodeServer { + if (Test-PodeCancellationTokenRequest -Type Disable) { + Reset-PodeCancellationToken -Type Disable + } +} + +<# +.SYNOPSIS + Blocks new incoming requests by adding middleware that returns a 503 Service Unavailable status when the Pode Watchdog client is active. + +.DESCRIPTION + This function integrates middleware into the Pode server, preventing new incoming requests while the Pode Watchdog client is active. + All requests receive a 503 Service Unavailable response, including a 'Retry-After' header that specifies when the service will become available. + +.PARAMETER RetryAfter + Specifies the time in seconds clients should wait before retrying their requests. Default is 3600 seconds (1 hour). +#> +function Disable-PodeServer { + param ( + [Parameter(Mandatory = $false)] + [int]$RetryAfter = 3600 + ) + + $PodeContext.Server.AllowedActions.DisableSettings.RetryAfter = $RetryAfter + if (! (Test-PodeCancellationTokenRequest -Type Disable)) { + Close-PodeCancellationTokenRequest -Type Disable + } +} + + diff --git a/src/Public/Endpoint.ps1 b/src/Public/Endpoint.ps1 new file mode 100644 index 000000000..d6b1c97c1 --- /dev/null +++ b/src/Public/Endpoint.ps1 @@ -0,0 +1,592 @@ + +<# +.SYNOPSIS +Bind an endpoint to listen for incoming Requests. + +.DESCRIPTION +Bind an endpoint to listen for incoming Requests. The endpoints can be HTTP, HTTPS, TCP or SMTP, with the option to bind certificates. + +.PARAMETER Address +The IP/Hostname of the endpoint (Default: localhost). + +.PARAMETER Port +The Port number of the endpoint. + +.PARAMETER Hostname +An optional hostname for the endpoint, specifying a hostname restricts access to just the hostname. + +.PARAMETER Protocol +The protocol of the supplied endpoint. + +.PARAMETER Certificate +The path to a certificate that can be use to enable HTTPS + +.PARAMETER CertificatePassword +The password for the certificate file referenced in Certificate + +.PARAMETER CertificateKey +A key file to be paired with a PEM certificate file referenced in Certificate + +.PARAMETER CertificateThumbprint +A certificate thumbprint to bind onto HTTPS endpoints (Windows). + +.PARAMETER CertificateName +A certificate subject name to bind onto HTTPS endpoints (Windows). + +.PARAMETER CertificateStoreName +The name of a certifcate store where a certificate can be found (Default: My) (Windows). + +.PARAMETER CertificateStoreLocation +The location of a certifcate store where a certificate can be found (Default: CurrentUser) (Windows). + +.PARAMETER X509Certificate +The raw X509 certificate that can be use to enable HTTPS + +.PARAMETER TlsMode +The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit). + +.PARAMETER Name +An optional name for the endpoint, that can be used with other functions (Default: GUID). + +.PARAMETER RedirectTo +The Name of another Endpoint to automatically generate a redirect route for all traffic. + +.PARAMETER Description +A quick description of the Endpoint - normally used in OpenAPI. + +.PARAMETER Acknowledge +An optional Acknowledge message to send to clients when they first connect, for TCP and SMTP endpoints only. + +.PARAMETER SslProtocol +One or more optional SSL Protocols this endpoints supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS). + +.PARAMETER CRLFMessageEnd +If supplied, TCP endpoints will expect incoming data to end with CRLF. + +.PARAMETER Force +Ignore Adminstrator checks for non-localhost endpoints. + +.PARAMETER SelfSigned +Create and bind a self-signed certifcate for HTTPS endpoints. + +.PARAMETER AllowClientCertificate +Allow for client certificates to be sent on requests. + +.PARAMETER PassThru +If supplied, the endpoint created will be returned. + +.PARAMETER LookupHostname +If supplied, a supplied Hostname will have its IP Address looked up from host file or DNS. + +.PARAMETER DualMode +If supplied, this endpoint will listen on both the IPv4 and IPv6 versions of the supplied -Address. +For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 address. + +.PARAMETER Default +If supplied, this endpoint will be the default one used for internally generating URLs. + +.EXAMPLE +Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http + +.EXAMPLE +Add-PodeEndpoint -Address localhost -Protocol Smtp + +.EXAMPLE +Add-PodeEndpoint -Address dev.pode.com -Port 8443 -Protocol Https -SelfSigned + +.EXAMPLE +Add-PodeEndpoint -Address 127.0.0.2 -Hostname dev.pode.com -Port 8443 -Protocol Https -SelfSigned + +.EXAMPLE +Add-PodeEndpoint -Address live.pode.com -Protocol Https -CertificateThumbprint '2A9467F7D3940243D6C07DE61E7FCCE292' +#> +function Add-PodeEndpoint { + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([hashtable])] + param( + [Parameter()] + [string] + $Address = 'localhost', + + [Parameter()] + [int] + $Port = 0, + + [Parameter()] + [string] + $Hostname, + + [Parameter()] + [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')] + [string] + $Protocol, + + [Parameter(Mandatory = $true, ParameterSetName = 'CertFile')] + [string] + $Certificate = $null, + + [Parameter(ParameterSetName = 'CertFile')] + [string] + $CertificatePassword = $null, + + [Parameter(ParameterSetName = 'CertFile')] + [string] + $CertificateKey = $null, + + [Parameter(Mandatory = $true, ParameterSetName = 'CertThumb')] + [string] + $CertificateThumbprint, + + [Parameter(Mandatory = $true, ParameterSetName = 'CertName')] + [string] + $CertificateName, + + [Parameter(ParameterSetName = 'CertName')] + [Parameter(ParameterSetName = 'CertThumb')] + [System.Security.Cryptography.X509Certificates.StoreName] + $CertificateStoreName = 'My', + + [Parameter(ParameterSetName = 'CertName')] + [Parameter(ParameterSetName = 'CertThumb')] + [System.Security.Cryptography.X509Certificates.StoreLocation] + $CertificateStoreLocation = 'CurrentUser', + + [Parameter(Mandatory = $true, ParameterSetName = 'CertRaw')] + [X509Certificate] + $X509Certificate = $null, + + [Parameter(ParameterSetName = 'CertFile')] + [Parameter(ParameterSetName = 'CertThumb')] + [Parameter(ParameterSetName = 'CertName')] + [Parameter(ParameterSetName = 'CertRaw')] + [Parameter(ParameterSetName = 'CertSelf')] + [ValidateSet('Implicit', 'Explicit')] + [string] + $TlsMode = 'Implicit', + + [Parameter()] + [string] + $Name = $null, + + [Parameter()] + [string] + $RedirectTo = $null, + + [Parameter()] + [string] + $Description, + + [Parameter()] + [string] + $Acknowledge, + + [Parameter()] + [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')] + [string[]] + $SslProtocol = $null, + + [switch] + $CRLFMessageEnd, + + [switch] + $Force, + + [Parameter(ParameterSetName = 'CertSelf')] + [switch] + $SelfSigned, + + [switch] + $AllowClientCertificate, + + [switch] + $PassThru, + + [switch] + $LookupHostname, + + [switch] + $DualMode, + + [switch] + $Default + ) + + # error if serverless + Test-PodeIsServerless -FunctionName 'Add-PodeEndpoint' -ThrowError + + # if RedirectTo is supplied, then a Name is mandatory + if (![string]::IsNullOrWhiteSpace($RedirectTo) -and [string]::IsNullOrWhiteSpace($Name)) { + # A Name is required for the endpoint if the RedirectTo parameter is supplied + throw ($PodeLocale.nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage) + } + + # get the type of endpoint + $type = Get-PodeEndpointType -Protocol $Protocol + + # are we running as IIS for HTTP/HTTPS? (if yes, force the port, address and protocol) + $isIIS = ((Test-PodeIsIIS) -and (@('Http', 'Ws') -icontains $type)) + if ($isIIS) { + $Port = [int]$env:ASPNETCORE_PORT + $Address = '127.0.0.1' + $Hostname = [string]::Empty + $Protocol = $type + } + + # are we running as Heroku for HTTP/HTTPS? (if yes, force the port, address and protocol) + $isHeroku = ((Test-PodeIsHeroku) -and (@('Http') -icontains $type)) + if ($isHeroku) { + $Port = [int]$env:PORT + $Address = '0.0.0.0' + $Hostname = [string]::Empty + $Protocol = $type + } + + # parse the endpoint for host/port info + if (![string]::IsNullOrWhiteSpace($Hostname) -and !(Test-PodeHostname -Hostname $Hostname)) { + # Invalid hostname supplied + throw ($PodeLocale.invalidHostnameSuppliedExceptionMessage -f $Hostname) + } + + if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) { + $Hostname = $Address + $Address = 'localhost' + } + + if (![string]::IsNullOrWhiteSpace($Hostname) -and $LookupHostname) { + $Address = (Get-PodeIPAddressesForHostname -Hostname $Hostname -Type All | Select-Object -First 1) + } + + $_endpoint = Get-PodeEndpointInfo -Address "$($Address):$($Port)" + + # if no name, set to guid, then check uniqueness + if ([string]::IsNullOrWhiteSpace($Name)) { + $Name = New-PodeGuid -Secure + } + + if ($PodeContext.Server.Endpoints.ContainsKey($Name)) { + # An endpoint named has already been defined + throw ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f $Name) + } + + # protocol must be https for client certs, or hosted behind a proxy like iis + if (($Protocol -ine 'https') -and !(Test-PodeIsHosted) -and $AllowClientCertificate) { + # Client certificates are only supported on HTTPS endpoints + throw ($PodeLocale.clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage) + } + + # explicit tls is only supported for smtp/tcp + if (($type -inotin @('smtp', 'tcp')) -and ($TlsMode -ieq 'explicit')) { + # The Explicit TLS mode is only supported on SMTPS and TCPS endpoints + throw ($PodeLocale.explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage) + } + + # ack message is only for smtp/tcp + if (($type -inotin @('smtp', 'tcp')) -and ![string]::IsNullOrEmpty($Acknowledge)) { + # The Acknowledge message is only supported on SMTP and TCP endpoints + throw ($PodeLocale.acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage) + } + + # crlf message end is only for tcp + if (($type -ine 'tcp') -and $CRLFMessageEnd) { + # The CRLF message end check is only supported on TCP endpoints + throw ($PodeLocale.crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage) + } + + # new endpoint object + $obj = @{ + Name = $Name + Description = $Description + DualMode = $DualMode + Address = $null + RawAddress = $null + Port = $null + IsIPAddress = $true + HostName = $Hostname + FriendlyName = $Hostname + Url = $null + Ssl = @{ + Enabled = (@('https', 'wss', 'smtps', 'tcps') -icontains $Protocol) + Protocols = $PodeContext.Server.Sockets.Ssl.Protocols + } + Protocol = $Protocol.ToLowerInvariant() + Type = $type.ToLowerInvariant() + Runspace = @{ + PoolName = (Get-PodeEndpointRunspacePoolName -Protocol $Protocol) + } + Default = $Default.IsPresent + Certificate = @{ + Raw = $X509Certificate + SelfSigned = $SelfSigned + AllowClientCertificate = $AllowClientCertificate + TlsMode = $TlsMode + } + Tcp = @{ + Acknowledge = $Acknowledge + CRLFMessageEnd = $CRLFMessageEnd + } + } + + # set ssl protocols + if (!(Test-PodeIsEmpty $SslProtocol)) { + $obj.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $SslProtocol) + } + + # set the ip for the context (force to localhost for IIS) + $obj.Address = Get-PodeIPAddress $_endpoint.Host -DualMode:$DualMode + $obj.IsIPAddress = [string]::IsNullOrWhiteSpace($obj.HostName) + + if ($obj.IsIPAddress) { + if (!(Test-PodeIPAddressLocalOrAny -IP $obj.Address)) { + $obj.FriendlyName = "$($obj.Address)" + } + else { + $obj.FriendlyName = 'localhost' + } + } + + # set the port for the context, if 0 use a default port for protocol + $obj.Port = $_endpoint.Port + if (([int]$obj.Port) -eq 0) { + $obj.Port = Get-PodeDefaultPort -Protocol $Protocol -TlsMode $TlsMode + } + + if ($obj.IsIPAddress) { + $obj.RawAddress = "$($obj.Address):$($obj.Port)" + } + else { + $obj.RawAddress = "$($obj.FriendlyName):$($obj.Port)" + } + + # set the url of this endpoint + 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 + throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) + } + + # has this endpoint been added before? (for http/https we can just not add it again) + $exists = ($PodeContext.Server.Endpoints.Values | Where-Object { + ($_.FriendlyName -ieq $obj.FriendlyName) -and ($_.Port -eq $obj.Port) -and ($_.Ssl.Enabled -eq $obj.Ssl.Enabled) -and ($_.Type -ieq $obj.Type) + } | Measure-Object).Count + + # if we're dealing with a certificate, attempt to import it + if (!(Test-PodeIsHosted) -and ($PSCmdlet.ParameterSetName -ilike 'cert*')) { + # fail if protocol is not https + if (@('https', 'wss', 'smtps', 'tcps') -inotcontains $Protocol) { + # Certificate supplied for non-HTTPS/WSS endpoint + throw ($PodeLocale.certificateSuppliedForNonHttpsWssEndpointExceptionMessage) + } + + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'certfile' { + $obj.Certificate.Raw = Get-PodeCertificateByFile -Certificate $Certificate -Password $CertificatePassword -Key $CertificateKey + } + + 'certthumb' { + $obj.Certificate.Raw = Get-PodeCertificateByThumbprint -Thumbprint $CertificateThumbprint -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation + } + + 'certname' { + $obj.Certificate.Raw = Get-PodeCertificateByName -Name $CertificateName -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation + } + + 'certself' { + $obj.Certificate.Raw = New-PodeSelfSignedCertificate + } + } + + # fail if the cert is expired + if ($obj.Certificate.Raw.NotAfter -lt [datetime]::Now) { + # The certificate has expired + throw ($PodeLocale.certificateExpiredExceptionMessage -f $obj.Certificate.Raw.Subject, $obj.Certificate.Raw.NotAfter) + } + } + + if (!$exists) { + # set server type + $_type = $type + if ($_type -iin @('http', 'ws')) { + $_type = 'http' + } + + if ($PodeContext.Server.Types -inotcontains $_type) { + $PodeContext.Server.Types += $_type + } + + # add the new endpoint + $PodeContext.Server.Endpoints[$Name] = $obj + $PodeContext.Server.EndpointsMap["$($obj.Protocol)|$($obj.RawAddress)"] = $Name + } + + # if RedirectTo is set, attempt to build a redirecting route + if (!(Test-PodeIsHosted) -and ![string]::IsNullOrWhiteSpace($RedirectTo)) { + $redir_endpoint = $PodeContext.Server.Endpoints[$RedirectTo] + + # ensure the name exists + if (Test-PodeIsEmpty $redir_endpoint) { + # An endpoint named has not been defined for redirecting + throw ($PodeLocale.endpointNotDefinedForRedirectingExceptionMessage -f $RedirectTo) + } + + # build the redirect route + Add-PodeRoute -Method * -Path * -EndpointName $obj.Name -ArgumentList $redir_endpoint -ScriptBlock { + param($endpoint) + Move-PodeResponseUrl -EndpointName $endpoint.Name + } + } + + # return the endpoint? + if ($PassThru) { + return $obj + } +} + +<# +.SYNOPSIS +Get an Endpoint(s). + +.DESCRIPTION +Get an Endpoint(s). + +.PARAMETER Address +An Address to filter the endpoints. + +.PARAMETER Port +A Port to filter the endpoints. + +.PARAMETER Hostname +A Hostname to filter the endpoints. + +.PARAMETER Protocol +A Protocol to filter the endpoints. + +.PARAMETER Name +Any endpoints Names to filter endpoints. + +.EXAMPLE +Get-PodeEndpoint -Address 127.0.0.1 + +.EXAMPLE +Get-PodeEndpoint -Protocol Http + +.EXAMPLE +Get-PodeEndpoint -Name Admin, User +#> +function Get-PodeEndpoint { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Address, + + [Parameter()] + [int] + $Port = 0, + + [Parameter()] + [string] + $Hostname, + + [Parameter()] + [ValidateSet('', 'Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')] + [string] + $Protocol, + + [Parameter()] + [string[]] + $Name + ) + + if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) { + $Hostname = $Address + $Address = 'localhost' + } + + $endpoints = $PodeContext.Server.Endpoints.Values + + # if we have an address, filter + if (![string]::IsNullOrWhiteSpace($Address)) { + if (($Address -eq '*') -or $PodeContext.Server.IsHeroku) { + $Address = '0.0.0.0' + } + + if ($PodeContext.Server.IsIIS -or ($Address -ieq 'localhost')) { + $Address = '127.0.0.1' + } + + $endpoints = @(foreach ($endpoint in $endpoints) { + if ($endpoint.Address.ToString() -ine $Address) { + continue + } + + $endpoint + }) + } + + # if we have a hostname, filter + if (![string]::IsNullOrWhiteSpace($Hostname)) { + $endpoints = @(foreach ($endpoint in $endpoints) { + if ($endpoint.Hostname.ToString() -ine $Hostname) { + continue + } + + $endpoint + }) + } + + # if we have a port, filter + if ($Port -gt 0) { + if ($PodeContext.Server.IsIIS) { + $Port = [int]$env:ASPNETCORE_PORT + } + + if ($PodeContext.Server.IsHeroku) { + $Port = [int]$env:PORT + } + + $endpoints = @(foreach ($endpoint in $endpoints) { + if ($endpoint.Port -ne $Port) { + continue + } + + $endpoint + }) + } + + # if we have a protocol, filter + if (![string]::IsNullOrWhiteSpace($Protocol)) { + if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) { + $Protocol = 'Http' + } + + $endpoints = @(foreach ($endpoint in $endpoints) { + if ($endpoint.Protocol -ine $Protocol) { + continue + } + + $endpoint + }) + } + + # further filter by endpoint names + if (($null -ne $Name) -and ($Name.Length -gt 0)) { + $endpoints = @(foreach ($_name in $Name) { + foreach ($endpoint in $endpoints) { + if ($endpoint.Name -ine $_name) { + continue + } + + $endpoint + } + }) + } + + # return + return $endpoints +} diff --git a/src/Public/Events.ps1 b/src/Public/Events.ps1 index b3f800a0d..e451c1999 100644 --- a/src/Public/Events.ps1 +++ b/src/Public/Events.ps1 @@ -24,8 +24,7 @@ function Register-PodeEvent { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')] - [string] + [Pode.PodeServerEventType] $Type, [Parameter(Mandatory = $true)] @@ -78,8 +77,7 @@ function Unregister-PodeEvent { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')] - [string] + [Pode.PodeServerEventType] $Type, [Parameter(Mandatory = $true)] @@ -116,8 +114,7 @@ function Test-PodeEvent { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')] - [string] + [Pode.PodeServerEventType] $Type, [Parameter(Mandatory = $true)] @@ -148,8 +145,7 @@ function Get-PodeEvent { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')] - [string] + [Pode.PodeServerEventType] $Type, [Parameter(Mandatory = $true)] @@ -177,8 +173,7 @@ function Clear-PodeEvent { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ValidateSet('Start', 'Terminate', 'Restart', 'Browser', 'Crash', 'Stop', 'Running')] - [string] + [Pode.PodeServerEventType] $Type ) diff --git a/src/Public/Metrics.ps1 b/src/Public/Metrics.ps1 index 0d47243d4..2334f27c5 100644 --- a/src/Public/Metrics.ps1 +++ b/src/Public/Metrics.ps1 @@ -1,35 +1,93 @@ <# .SYNOPSIS -Returns the uptime of the server in milliseconds. + Retrieves the server uptime in milliseconds or a human-readable format. .DESCRIPTION -Returns the uptime of the server in milliseconds. You can optionally return the total uptime regardless of server restarts. + The `Get-PodeServerUptime` function calculates the server's uptime since its last start or total uptime since initial load, depending on the `-Total` switch. + By default, the uptime is returned in milliseconds. When the `-Format` parameter is used, the uptime can be returned in various human-readable styles: + - `Milliseconds` (default): Raw uptime in milliseconds. + - `Concise`: A short format like "1d 2h 3m". + - `Compact`: A condensed format like "01:10:17:36". + - `Verbose`: A detailed format like "1 day, 2 hours, 3 minutes, 5 seconds, 200 milliseconds". + The `-ExcludeMilliseconds` switch allows removal of milliseconds from human-readable output. .PARAMETER Total -If supplied, the total uptime of the server will be returned, regardless of restarts. + Retrieves the total server uptime since the initial load, regardless of any restarts. + +.PARAMETER Format + Specifies the desired output format for the uptime. + Allowed values: + - `Milliseconds` (default): Uptime in raw milliseconds. + - `Concise`: Human-readable in a short form (e.g., "1d 2h 3m"). + - `Compact`: Condensed form (e.g., "01:10:17:36"). + - `Verbose`: Detailed format (e.g., "1 day, 2 hours, 3 minutes, 5 seconds"). + +.PARAMETER ExcludeMilliseconds + Omits milliseconds from the human-readable output when `-Format` is not `Milliseconds`. + +.EXAMPLE + $currentUptime = Get-PodeServerUptime + # Output: 123456789 (milliseconds) + +.EXAMPLE + $totalUptime = Get-PodeServerUptime -Total + # Output: 987654321 (milliseconds) .EXAMPLE -$currentUptime = Get-PodeServerUptime + $readableUptime = Get-PodeServerUptime -Format Concise + # Output: "1d 10h 17m" .EXAMPLE -$totalUptime = Get-PodeServerUptime -Total + $verboseUptime = Get-PodeServerUptime -Format Verbose + # Output: "1 day, 10 hours, 17 minutes, 36 seconds, 789 milliseconds" + +.EXAMPLE + $compactUptime = Get-PodeServerUptime -Format Compact + # Output: "01:10:17:36" + +.EXAMPLE + $compactUptimeNoMs = Get-PodeServerUptime -Format Compact -ExcludeMilliseconds + # Output: "01:10:17:36" + +.NOTES + This function is part of Pode's utility metrics to monitor server uptime. #> function Get-PodeServerUptime { [CmdletBinding()] - [OutputType([long])] + [OutputType([long], [string])] param( [switch] - $Total + $Total, + + [Parameter()] + [ValidateSet('Milliseconds', 'Concise', 'Compact', 'Verbose')] + [string] + $Format = 'Milliseconds', + + [switch] + $ExcludeMilliseconds ) + # Determine the start time based on the -Total switch + # Default: Uses the last start time; -Total: Uses the initial load time $time = $PodeContext.Metrics.Server.StartTime if ($Total) { $time = $PodeContext.Metrics.Server.InitialLoadTime } - return [long]([datetime]::UtcNow - $time).TotalMilliseconds + # Calculate uptime in milliseconds + $uptimeMilliseconds = [long]([datetime]::UtcNow - $time).TotalMilliseconds + + # Return uptime in milliseconds if no readable format is requested + if ($Format -ieq 'Milliseconds') { + return $uptimeMilliseconds + } + + # Convert uptime to a human-readable format + return Convert-PodeMillisecondsToReadable -Milliseconds $uptimeMilliseconds -Format $Format -ExcludeMilliseconds:$ExcludeMilliseconds } + <# .SYNOPSIS Returns the number of times the server has restarted. diff --git a/src/Public/Middleware.ps1 b/src/Public/Middleware.ps1 index 8b7f8d15d..e8320fefd 100644 --- a/src/Public/Middleware.ps1 +++ b/src/Public/Middleware.ps1 @@ -644,4 +644,43 @@ function Use-PodeMiddleware { ) Use-PodeFolder -Path $Path -DefaultPath 'middleware' -} \ No newline at end of file +} + + + +<# +.SYNOPSIS + Checks if a specific middleware is registered in the Pode server. + +.DESCRIPTION + This function verifies whether a middleware with the specified name is registered in the Pode server by checking the `PodeContext.Server.Middleware` collection. + It returns `$true` if the middleware exists, otherwise it returns `$false`. + +.PARAMETER Name + The name of the middleware to check for. + +.OUTPUTS + [boolean] + Returns $true if the middleware with the specified name is found, otherwise returns $false. + +.EXAMPLE + Test-PodeMiddleware -Name 'BlockEverything' + + This command checks if a middleware named 'BlockEverything' is registered in the Pode server. +#> +function Test-PodeMiddleware { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # Check if the middleware exists + foreach ($middleware in $PodeContext.Server.Middleware) { + if ($middleware.Name -ieq $Name) { + return $true + } + } + + return $false +} diff --git a/src/Public/OpenApi.ps1 b/src/Public/OpenApi.ps1 index 3c3940b32..46be78269 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) } } } @@ -3738,8 +3738,6 @@ Validate the OpenAPI definition if all Reference are satisfied .DESCRIPTION Validate the OpenAPI definition if all Reference are satisfied - - .PARAMETER DefinitionTag An Array of strings representing the unique tag for the API specification. This tag helps distinguish between different versions or types of API specifications within the application. @@ -3793,4 +3791,38 @@ function Test-PodeOADefinition { } } return $result +} + + + +<# +.SYNOPSIS + Checks if OpenAPI is enabled in the Pode server. + +.DESCRIPTION + The `Test-PodeOAEnabled` function iterates through the OpenAPI definitions in the Pode server to determine if any are enabled. + It checks for the presence of `bookmarks` in the hidden components of each definition, which indicates an active OpenAPI configuration. + +.RETURNS + [bool] True if OpenAPI is enabled; otherwise, False. + +.EXAMPLE + Test-PodeOAEnabled + + Returns $true if OpenAPI is enabled for any definition in the Pode server, otherwise returns $false. +#> +function Test-PodeOAEnabled { + # Iterate through each OpenAPI definition key + foreach ($key in $PodeContext.Server.OpenAPI.Definitions.Keys) { + # Retrieve the bookmarks from the hidden components + $bookmarks = $PodeContext.Server.OpenAPI.Definitions[$key].hiddenComponents.bookmarks + + # If bookmarks exist, OpenAPI is enabled for this definition + if ($bookmarks) { + return $true + } + } + + # If no bookmarks are found, OpenAPI is not enabled + return $false } \ No newline at end of file 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 diff --git a/src/Public/Schedules.ps1 b/src/Public/Schedules.ps1 index 974a0341c..3dd8c21ef 100644 --- a/src/Public/Schedules.ps1 +++ b/src/Public/Schedules.ps1 @@ -132,6 +132,9 @@ function Add-PodeSchedule { # check for scoped vars $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + # Modify the ScriptBlock to replace 'Start-Sleep' with 'Start-PodeSleep' + $ScriptBlock = ConvertTo-PodeSleep -ScriptBlock $ScriptBlock + # add the schedule $parsedCrons = ConvertFrom-PodeCronExpression -Expression @($Cron) $nextTrigger = Get-PodeCronNextEarliestTrigger -Expressions $parsedCrons -StartTime $StartTime -EndTime $EndTime diff --git a/src/Public/Tasks.ps1 b/src/Public/Tasks.ps1 index c0267f9be..3db1e0268 100644 --- a/src/Public/Tasks.ps1 +++ b/src/Public/Tasks.ps1 @@ -68,6 +68,9 @@ function Add-PodeTask { $ScriptBlock = Convert-PodeFileToScriptBlock -FilePath $FilePath } + # Modify the ScriptBlock to replace 'Start-Sleep' with 'Start-PodeSleep' + $ScriptBlock = ConvertTo-PodeSleep -ScriptBlock $ScriptBlock + # check for scoped vars $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 893ffd464..e193bbbc3 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -20,7 +20,7 @@ Close-PodeDisposable -Disposable $stream -Close function Close-PodeDisposable { [CmdletBinding()] param( - [Parameter()] + [Parameter(ValueFromPipeline = $true)] [System.IDisposable] $Disposable, @@ -30,26 +30,27 @@ function Close-PodeDisposable { [switch] $CheckNetwork ) + process { + if ($null -eq $Disposable) { + return + } - if ($null -eq $Disposable) { - return - } + try { + if ($Close) { + $Disposable.Close() + } + } + catch [exception] { + if ($CheckNetwork -and (Test-PodeValidNetworkFailure $_.Exception)) { + return + } - try { - if ($Close) { - $Disposable.Close() + $_ | Write-PodeErrorLog + throw $_.Exception } - } - catch [exception] { - if ($CheckNetwork -and (Test-PodeValidNetworkFailure $_.Exception)) { - return + finally { + $Disposable.Dispose() } - - $_ | Write-PodeErrorLog - throw $_.Exception - } - finally { - $Disposable.Dispose() } } @@ -421,19 +422,46 @@ function Import-PodeSnapin { <# .SYNOPSIS -Protects a value, by returning a default value is the main one is null/empty. + Resolves and protects a value by ensuring it defaults to a specified fallback and optionally parses it as an enum. .DESCRIPTION -Protects a value, by returning a default value is the main one is null/empty. + The `Protect-PodeValue` function ensures that a given value is resolved. If the value is empty, a default value is used instead. + Additionally, the function can parse the resolved value as an enum type with optional case sensitivity. .PARAMETER Value -The main value to use. + The input value to be resolved. .PARAMETER Default -A default value to return should the main value be null/empty. + The default value to fall back to if the input value is empty. + +.PARAMETER EnumType + The type of enum to parse the resolved value into. If specified, the resolved value must be a valid enum member. + +.PARAMETER CaseSensitive + Specifies whether the enum parsing should be case-sensitive. By default, parsing is case-insensitive. + +.OUTPUTS + [object] + Returns the resolved value, either as the original value, the default value, or a parsed enum. .EXAMPLE -$Name = Protect-PodeValue -Value $Name -Default 'Rick' + # Example 1: Resolve a value with a default fallback + $resolved = Protect-PodeValue -Value $null -Default "Fallback" + Write-Output $resolved # Output: Fallback + +.EXAMPLE + # Example 2: Resolve and parse a value as a case-insensitive enum + $resolvedEnum = Protect-PodeValue -Value "red" -Default "Blue" -EnumType ([type][System.ConsoleColor]) + Write-Output $resolvedEnum # Output: Red + +.EXAMPLE + # Example 3: Resolve and parse a value as a case-sensitive enum + $resolvedEnum = Protect-PodeValue -Value "red" -Default "Blue" -EnumType ([type][System.ConsoleColor]) -CaseSensitive + # Throws an error if "red" does not match an enum member exactly (case-sensitive). + +.NOTES + This function resolves values using `Resolve-PodeValue` and validates enums using `[enum]::IsDefined`. + #> function Protect-PodeValue { [CmdletBinding()] @@ -443,10 +471,24 @@ function Protect-PodeValue { $Value, [Parameter()] - $Default + $Default, + + [Parameter()] + [Type] + $EnumType, + + [switch] + $CaseSensitive ) - return (Resolve-PodeValue -Check (Test-PodeIsEmpty $Value) -TrueValue $Default -FalseValue $Value) + $resolvedValue = Resolve-PodeValue -Check (Test-PodeIsEmpty $Value) -TrueValue $Default -FalseValue $Value + + if ($null -ne $EnumType -and [enum]::IsDefined($EnumType, $resolvedValue)) { + # Use $CaseSensitive to determine if case sensitivity should apply + return [enum]::Parse($EnumType, $resolvedValue, !$CaseSensitive.IsPresent) + } + + return $resolvedValue } <# @@ -814,7 +856,7 @@ function Out-PodeHost { } end { - if ($PodeContext.Server.Quiet) { + if ($PodeContext.Server.Console.Quiet) { return } # Set InputObject to the array of values @@ -855,6 +897,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 +928,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 +944,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 @@ -1486,3 +1534,80 @@ function Invoke-PodeGC { [System.GC]::Collect() } + +<# +.SYNOPSIS + A function to pause execution for a specified duration. + This function should be used in Pode as replacement for Start-Sleep + +.DESCRIPTION + The `Start-PodeSleep` function pauses script execution for a given duration specified in seconds, milliseconds, or a TimeSpan. + +.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 + + Pauses execution for 5 seconds. + +.NOTES + This function is useful for scenarios where tracking the remaining wait time visually is helpful. +#> +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 the total duration + $totalDuration = switch ($PSCmdlet.ParameterSetName) { + 'Seconds' { [TimeSpan]::FromSeconds($Seconds) } + 'Milliseconds' { [TimeSpan]::FromMilliseconds($Milliseconds) } + 'Duration' { $Duration } + } + + # Calculate end time + $startTime = [DateTime]::UtcNow + $endTime = $startTime.Add($totalDuration) + + # Precompute sleep interval (total duration divided by 100 - ie 100%) + $sleepInterval = [math]::Max($totalDuration.TotalMilliseconds / 100, 10) + + # Main loop + while ([DateTime]::UtcNow -lt $endTime) { + # Sleep for the interval + Start-Sleep -Milliseconds $sleepInterval + } +} + + + diff --git a/tests/integration/OpenApi.Tests.ps1 b/tests/integration/OpenApi.Tests.ps1 index a6e4a7e97..cc398cdd4 100644 --- a/tests/integration/OpenApi.Tests.ps1 +++ b/tests/integration/OpenApi.Tests.ps1 @@ -18,8 +18,8 @@ Describe 'OpenAPI integration tests' { } $PortV3 = 8080 $PortV3_1 = 8081 - $scriptPath = "$($PSScriptRoot)\..\..\examples\OpenApi-TuttiFrutti.ps1" - Start-Process (Get-Process -Id $PID).Path -ArgumentList "-NoProfile -File `"$scriptPath`" -Quiet -PortV3 $PortV3 -PortV3_1 $PortV3_1 -DisableTermination" -NoNewWindow + $scriptPath = "$($PSScriptRoot)\..\..\examples\OpenApi-TuttiFrutti.ps1" + Start-Process (Get-Process -Id $PID).Path -ArgumentList "-NoProfile -File `"$scriptPath`" -PortV3 $PortV3 -PortV3_1 $PortV3_1 -Daemon -IgnoreServerConfig" -NoNewWindow function Compare-StringRnLn { param ( diff --git a/tests/shared/TestHelper.ps1 b/tests/shared/TestHelper.ps1 new file mode 100644 index 000000000..38ccacbe2 --- /dev/null +++ b/tests/shared/TestHelper.ps1 @@ -0,0 +1,49 @@ +<# +.SYNOPSIS + Ensures the Pode assembly is loaded into the current session. + +.DESCRIPTION + This function checks if the Pode assembly is already loaded into the current PowerShell session. + If not, it determines the appropriate .NET runtime version and attempts to load the Pode.dll + from the most compatible directory. If no specific version is found, it defaults to netstandard2.0. + +.PARAMETER SrcPath + The base path where the Pode library (Libs folder) is located. + +.EXAMPLE + Import-PodeAssembly -SrcPath 'C:\Projects\MyApp' + Ensures that Pode.dll is loaded from the appropriate .NET folder. + +.NOTES + Ensure that the Pode library path is correctly structured with folders named + `netstandard2.0`, `net6.0`, etc., inside the `Libs` folder. +#> +function Import-PodeAssembly { + param ( + [Parameter(Mandatory = $true)] + [string]$SrcPath + ) + + # Check if Pode is already loaded + if (!([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' })) { + # Fetch the .NET runtime version + $version = [System.Environment]::Version.Major + $libsPath = Join-Path -Path $SrcPath -ChildPath 'Libs' + + # Filter .NET DLL folders based on version and get the latest one + $netFolder = if (![string]::IsNullOrWhiteSpace($version)) { + Get-ChildItem -Path $libsPath -Directory -Force | + Where-Object { $_.Name -imatch "net[1-$($version)]" } | + Sort-Object -Property Name -Descending | + Select-Object -First 1 -ExpandProperty FullName + } + + # Use netstandard2.0 if no folder found + if ([string]::IsNullOrWhiteSpace($netFolder)) { + $netFolder = Join-Path -Path $libsPath -ChildPath 'netstandard2.0' + } + + # Append Pode.dll and mount + Add-Type -LiteralPath (Join-Path -Path $netFolder -ChildPath 'Pode.dll') -ErrorAction Stop + } +} diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index 4c67439b8..55b97e75f 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1061,6 +1061,7 @@ Describe 'Get-PodeRelativePath' { Get-PodeRelativePath -Path './path' -JoinRoot | Should -Be 'c:/./path' } + It 'Returns resolved path for a relative path joined to default root when resolving' { $PodeContext = @{ Server = @{ @@ -1068,13 +1069,14 @@ Describe 'Get-PodeRelativePath' { } } - Get-PodeRelativePath -Path './src' -JoinRoot -Resolve | Should -Be (Join-Path $pwd.Path 'src') + Get-PodeRelativePath -Path './src' -JoinRoot -Resolve | Should -Be (Join-Path -Path $PWD -ChildPath 'src') } 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 (Join-Path -Path '.' -ChildPath 'path')-JoinRoot -RootPath $PWD | Should -Be (Join-Path -Path $PWD -ChildPath (Join-Path -Path '.' -ChildPath 'path')) } + It 'Throws error for path ot existing' { Mock Test-PodePath { return $false } { Get-PodeRelativePath -Path './path' -TestPath } | Should -Throw -ExpectedMessage ($PodeLocale.pathNotExistExceptionMessage -f './path') # '*The path does not exist*' @@ -1138,29 +1140,21 @@ Describe 'Close-PodeRunspace' { Describe 'Close-PodeServerInternal' { BeforeAll { - Mock Close-PodeRunspace { } - Mock Stop-PodeFileMonitor { } - Mock Close-PodeDisposable { } - Mock Remove-PodePSDrive { } - Mock Write-Host { } } + Mock Close-PodeRunspace {} + Mock Stop-PodeFileMonitor {} + Mock Close-PodeDisposable {} + Mock Remove-PodePSDrive {} + Mock Write-PodeHost {} + Mock Close-PodeCancellationTokenRequest {} + } + It 'Closes out pode, but with no done flag' { $PodeContext = @{ 'Server' = @{ 'Types' = 'Server' } } Close-PodeServerInternal - Assert-MockCalled Write-Host -Times 0 -Scope It + Assert-MockCalled Write-PodeHost -Times 0 -Scope It } - It 'Closes out pode, but with the done flag' { - $PodeContext = @{ 'Server' = @{ 'Types' = 'Server' } } - Close-PodeServerInternal -ShowDoneMessage - Assert-MockCalled Write-Host -Times 1 -Scope It - } - - It 'Closes out pode, but with no done flag if serverless' { - $PodeContext = @{ 'Server' = @{ 'Types' = 'Server'; 'IsServerless' = $true } } - Close-PodeServerInternal -ShowDoneMessage - Assert-MockCalled Write-Host -Times 0 -Scope It - } } Describe 'Get-PodeEndpointUrl' { diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 6056b581c..8234f02d3 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -7,41 +7,49 @@ BeforeAll { Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + # Import Pode Assembly + $helperPath = (Split-Path -Parent -Path $path) -ireplace 'unit', 'shared' + . "$helperPath/TestHelper.ps1" + Import-PodeAssembly -SrcPath $src $PodeContext = @{ Server = $null Metrics = @{ Server = @{ StartTime = [datetime]::UtcNow } } RunspacePools = @{} - } } + Tokens = $null + } +} Describe 'Start-PodeInternalServer' { BeforeAll { - Mock Add-PodePSInbuiltDrive { } - Mock Invoke-PodeScriptBlock { } - Mock New-PodeRunspaceState { } - Mock New-PodeRunspacePool { } - Mock Start-PodeLoggingRunspace { } - Mock Start-PodeTimerRunspace { } - Mock Start-PodeScheduleRunspace { } - Mock Start-PodeGuiRunspace { } - Mock Start-Sleep { } - Mock New-PodeAutoRestartServer { } - Mock Start-PodeSmtpServer { } - Mock Start-PodeTcpServer { } - Mock Start-PodeWebServer { } - Mock Start-PodeServiceServer { } - Mock Import-PodeModulesIntoRunspaceState { } - Mock Import-PodeSnapinsIntoRunspaceState { } - Mock Import-PodeFunctionsIntoRunspaceState { } - Mock Start-PodeCacheHousekeeper { } - Mock Invoke-PodeEvent { } - Mock Write-Verbose { } - Mock Add-PodeScopedVariablesInbuilt { } - Mock Write-PodeHost { } + Mock Add-PodePSInbuiltDrive {} + Mock Invoke-PodeScriptBlock {} + Mock New-PodeRunspaceState {} + Mock New-PodeRunspacePool {} + Mock Start-PodeLoggingRunspace {} + Mock Start-PodeTimerRunspace {} + Mock Start-PodeScheduleRunspace {} + Mock Start-PodeGuiRunspace {} + Mock Start-Sleep {} + Mock New-PodeAutoRestartServer {} + Mock Start-PodeSmtpServer {} + Mock Start-PodeTcpServer {} + Mock Start-PodeWebServer {} + Mock Start-PodeServiceServer {} + Mock Import-PodeModulesIntoRunspaceState {} + Mock Import-PodeSnapinsIntoRunspaceState {} + Mock Import-PodeFunctionsIntoRunspaceState {} + Mock Start-PodeCacheHousekeeper {} + Mock Invoke-PodeEvent {} + Mock Write-Verbose {} + Mock Add-PodeScopedVariablesInbuilt {} + Mock Write-PodeHost {} + Mock Show-PodeConsoleInfo {} } It 'Calls one-off script logic' { - $PodeContext.Server = @{ Types = ([string]::Empty); Logic = {} } + $PodeContext.Server = @{ Types = ([string]::Empty); Logic = {}; Console = @{Quiet = $true }; EndpointsInfo = @() } + $PodeContext.Tokens = Initialize-PodeCancellationToken Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -55,7 +63,8 @@ Describe 'Start-PodeInternalServer' { } It 'Calls smtp server logic' { - $PodeContext.Server = @{ Types = 'SMTP'; Logic = {} } + $PodeContext.Server = @{ Types = 'SMTP'; Logic = {}; Console = @{Quiet = $true } ; EndpointsInfo = @() } + $PodeContext.Tokens = Initialize-PodeCancellationToken Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -69,7 +78,8 @@ Describe 'Start-PodeInternalServer' { } It 'Calls tcp server logic' { - $PodeContext.Server = @{ Types = 'TCP'; Logic = {} } + $PodeContext.Server = @{ Types = 'TCP'; Logic = {}; Console = @{Quiet = $true } ; EndpointsInfo = @() } + $PodeContext.Tokens = Initialize-PodeCancellationToken Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -83,7 +93,8 @@ Describe 'Start-PodeInternalServer' { } It 'Calls http web server logic' { - $PodeContext.Server = @{ Types = 'HTTP'; Logic = {} } + $PodeContext.Server = @{ Types = 'HTTP'; Logic = {}; Console = @{Quiet = $true } ; EndpointsInfo = @() } + $PodeContext.Tokens = Initialize-PodeCancellationToken Start-PodeInternalServer | Out-Null Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It @@ -99,22 +110,19 @@ Describe 'Start-PodeInternalServer' { Describe 'Restart-PodeInternalServer' { BeforeAll { - Mock Write-Host { } - Mock Close-PodeRunspace { } - Mock Remove-PodePSDrive { } + Mock Write-Host {} + Mock Close-PodeRunspace {} + Mock Remove-PodePSDrive {} Mock Open-PodeConfiguration { return $null } - Mock Start-PodeInternalServer { } - Mock Write-PodeErrorLog { } - Mock Close-PodeDisposable { } - Mock Invoke-PodeEvent { } + Mock Start-PodeInternalServer {} + Mock Write-PodeErrorLog {} + Mock Close-PodeDisposable {} + Mock Invoke-PodeEvent {} } It 'Resetting the server values' { $PodeContext = @{ - Tokens = @{ - Cancellation = [System.Threading.CancellationTokenSource]::new() - Restart = [System.Threading.CancellationTokenSource]::new() - } + Tokens = Initialize-PodeCancellationToken Server = @{ Routes = @{ GET = @{ 'key' = 'value' } @@ -151,7 +159,7 @@ Describe 'Restart-PodeInternalServer' { Output = @{ Variables = @{ 'key' = 'value' } } - Configuration = @{ 'key' = 'value' } + Configuration = @{ Enabled = $false; Server = @{'key' = 'value' } } Sockets = @{ Listeners = @() Queues = @{ @@ -203,6 +211,25 @@ Describe 'Restart-PodeInternalServer' { Storage = @{} } ScopedVariables = @{} + Console = @{ + DisableTermination = $true + DisableConsoleInput = $true + Quiet = $true + ClearHost = $false + ShowOpenAPI = $true + ShowEndpoints = $true + ShowHelp = $false + + } + AllowedActions = @{ + Suspend = $true + Restart = $true + Timeout = @{ + Suspend = 30 # timeout in seconds + Resume = 30 # timeout in seconds + } + } + } Metrics = @{ Server = @{ @@ -241,7 +268,7 @@ Describe 'Restart-PodeInternalServer' { Semaphores = @{} } } - + Restart-PodeServer Restart-PodeInternalServer | Out-Null $PodeContext.Server.Routes['GET'].Count | Should -Be 0 @@ -251,7 +278,9 @@ Describe 'Restart-PodeInternalServer' { $PodeContext.Server.Sessions.Count | Should -Be 0 $PodeContext.Server.Authentications.Methods.Count | Should -Be 0 $PodeContext.Server.State.Count | Should -Be 0 - $PodeContext.Server.Configuration | Should -Be $null + $PodeContext.Server.Configuration.Count | Should -Be 2 + $PodeContext.Server.Configuration.Enabled | Should -BeFalse + $PodeContext.Server.Configuration.Server.Key | Should -Be 'value' $PodeContext.Timers.Items.Count | Should -Be 0 $PodeContext.Schedules.Items.Count | Should -Be 0 @@ -264,10 +293,4 @@ Describe 'Restart-PodeInternalServer' { $PodeContext.Metrics.Server.RestartCount | Should -Be 1 } - - It 'Catches exception and throws it' { - Mock Write-Host { throw 'some error' } - Mock Write-PodeErrorLog {} - { Restart-PodeInternalServer } | Should -Throw -ExpectedMessage 'some error' - } } \ No newline at end of file diff --git a/tests/unit/_.Tests.ps1 b/tests/unit/_.Tests.ps1 index 74a8cea04..917b4fe00 100644 --- a/tests/unit/_.Tests.ps1 +++ b/tests/unit/_.Tests.ps1 @@ -8,7 +8,7 @@ BeforeDiscovery { # List of directories to exclude $excludeDirs = @('scripts', 'views', 'static', 'public', 'assets', 'timers', 'modules', - 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues','auth') + 'Authentication', 'certs', 'logs', 'relative', 'routes', 'issues', 'auth') # Convert exlusion list into single regex pattern for directory matching $dirSeparator = [IO.Path]::DirectorySeparatorChar @@ -24,6 +24,11 @@ BeforeDiscovery { BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + + # Import Pode Assembly + $helperPath = (Split-Path -Parent -Path $path) -ireplace 'unit', 'shared' + . "$helperPath/TestHelper.ps1" + Import-PodeAssembly -SrcPath $src # public functions $sysFuncs = Get-ChildItem Function: @@ -233,4 +238,4 @@ Describe 'Check for Duplicate Function Definitions' { # Assert no duplicate function definitions $duplicatedFunctionNames.Count | Should -Be 0 } -} +} \ No newline at end of file