From 13511737038cc7994d476da3a3a6aa4b2c986d5e Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 3 Mar 2025 05:54:55 -0800 Subject: [PATCH 1/4] Squashed changes from PR #1507 --- src/Private/Logging.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index 394311e4a..3b35edb35 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -63,10 +63,10 @@ function Get-PodeLoggingFileMethod { $item.ToString() | Out-File -FilePath $path -Encoding utf8 -Append -Force # if set, remove log files beyond days set (ensure this is only run once a day) - if (($options.MaxDays -gt 0) -and ($options.NextClearDown -lt [DateTime]::Now.Date)) { + if (($options.MaxDays -gt 0) -and ($options.NextClearDown -le [DateTime]::Now.Date)) { $date = [DateTime]::Now.Date.AddDays(-$options.MaxDays) - $null = Get-ChildItem -Path $options.Path -Filter '*.log' -Force | + $null = Get-ChildItem -Path $options.Path -Filter "$($options.Name)_*.log" -Force | Where-Object { $_.CreationTime -lt $date } | Remove-Item -Force From 06c8764c20ab5f6997af6b8bf80536c4360fd3eb Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 3 Mar 2025 05:57:48 -0800 Subject: [PATCH 2/4] Squashed changes from PR #1504 --- examples/OpenApi-TuttiFrutti.ps1 | 16 +++--- pode.build.ps1 | 26 +++++++-- src/Pode.Internal.psm1 | 5 +- src/Pode.psd1 | 3 + src/Pode.psm1 | 20 +++++-- src/Private/Helpers.ps1 | 72 ++++++++++++++---------- src/Public/OAComponents.ps1 | 14 +---- src/Public/OAProperties.ps1 | 5 -- src/Public/Utilities.ps1 | 47 +++++++++------- tests/unit/OpenApi.Tests.ps1 | 96 ++++++++++++++++---------------- 10 files changed, 173 insertions(+), 131 deletions(-) diff --git a/examples/OpenApi-TuttiFrutti.ps1 b/examples/OpenApi-TuttiFrutti.ps1 index e80ff8023..d5c739e60 100644 --- a/examples/OpenApi-TuttiFrutti.ps1 +++ b/examples/OpenApi-TuttiFrutti.ps1 @@ -162,7 +162,7 @@ Some useful links: New-PodeOAStringProperty -Name 'shipDate' -Format Date-Time | New-PodeOAStringProperty -Name 'status' -Description 'Order Status' -Example 'approved' -Enum @('placed', 'approved', 'delivered') | New-PodeOABoolProperty -Name 'complete' | - New-PodeOASchemaProperty -Name 'Address' -Reference 'Address' | + New-PodeOAComponentSchemaProperty -Name 'Address' -Reference 'Address' | New-PodeOAObjectProperty -Name 'Order' -XmlName 'order' -AdditionalProperties (New-PodeOAStringProperty ) | Add-PodeOAComponentSchema -Name 'Order' @@ -196,10 +196,10 @@ Some useful links: New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' -Properties ( New-PodeOAIntProperty -Name 'id'-Format Int64 -Example @(10, 2, 4) -ReadOnly | New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required | - New-PodeOASchemaProperty -Name 'category' -Reference 'Category' | + New-PodeOAComponentSchemaProperty -Name 'category' -Reference 'Category' | New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required | New-PodeOAStringProperty -Name 'photoUrls' -Array | - New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag' | + New-PodeOAComponentSchemaProperty -Name 'tags' -Reference 'Tag' | New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold') )) @@ -226,7 +226,7 @@ Some useful links: New-PodeOAStringProperty -Name 'name' | New-PodeOAStringProperty -Name 'type' | - New-PodeOASchemaProperty -Name 'children' -Array -Reference 'StructPart' | + New-PodeOAComponentSchemaProperty -Name 'children' -Array -Reference 'StructPart' | New-PodeOAObjectProperty | Add-PodeOAComponentSchema -Name 'StructPart' @@ -252,10 +252,10 @@ Some useful links: New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' } -Properties @( (New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -ReadOnly), (New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required), - (New-PodeOASchemaProperty -Name 'category' -Component 'Category'), + (New-PodeOAComponentSchemaProperty -Name 'category' -Component 'Category'), (New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required), (New-PodeOAStringProperty -Name 'photoUrls' -Array), - (New-PodeOASchemaProperty -Name 'tags' -Component 'Tag') + (New-PodeOAComponentSchemaProperty -Name 'tags' -Component 'Tag') (New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold')) )) #> @@ -309,7 +309,7 @@ Some useful links: Set-PodeOARouteInfo -Summary 'Find pets by ID' -Description 'Returns pets based on ID' -OperationId 'getPetsById' -PassThru | Set-PodeOARequest -PassThru -Parameters @( ( New-PodeOAStringProperty -Name 'id' -Description 'ID of pet to use' -array | ConvertTo-PodeOAParameter -In Path -Style Simple -Required )) | - Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOASchemaProperty -Reference 'Pet' -array }) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'pet response' -Content (@{ '*/*' = New-PodeOAComponentSchemaProperty -Reference 'Pet' -array }) -PassThru | Add-PodeOAResponse -Default -Description 'error payload' -Content (@{ 'text/html' = 'ApiResponse' }) -PassThru @@ -746,7 +746,7 @@ Some useful links: New-PodeOAStringProperty -name 'id' -format 'uuid' | New-PodeOAObjectProperty -name 'address' -NoProperties | New-PodeOAStringProperty -name 'children' -array | - New-PodeOASchemaProperty -Name 'addresses' -Reference 'Address' -Array | + New-PodeOAComponentSchemaProperty -Name 'addresses' -Reference 'Address' -Array | New-PodeOAObjectProperty }) | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -PassThru | Add-PodeOAResponse -StatusCode 400 -Description 'Invalid ID supplied' -PassThru | diff --git a/pode.build.ps1 b/pode.build.ps1 index ae8862739..291e34892 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -9,6 +9,10 @@ .PARAMETER Version Specifies the project version for stamping, packaging, and documentation. Defaults to '0.0.0'. +.PARAMETER Prerelease + Specifies the prerelease label to append to the module version, following semantic versioning conventions. + Examples include 'alpha.1', 'alpha.2', 'beta.1', etc. This label indicates the stability and iteration of the prerelease version. + .PARAMETER PesterVerbosity Sets the verbosity level for Pester tests. Options: None, Normal, Detailed, Diagnostic. @@ -90,6 +94,9 @@ param( [string] $Version = '0.0.0', + [string] + $Prerelease, + [string] [ValidateSet('None', 'Normal' , 'Detailed', 'Diagnostic')] $PesterVerbosity = 'Normal', @@ -487,7 +494,15 @@ function Invoke-PodeBuildDotnetBuild { # Optionally set assembly version if ($Version) { Write-Output "Assembly Version: $Version" - $AssemblyVersion = "-p:Version=$Version" + + if ($Prerelease) { + $AssemblyVersion = "-p:VersionPrefix=$Version" + $AssemblyPrerelease = "-p:VersionSuffix=$Prerelease" + } + else { + $AssemblyVersion = "-p:Version=$Version" + $AssemblyPrerelease = '' + } } else { $AssemblyVersion = '' @@ -497,7 +512,7 @@ function Invoke-PodeBuildDotnetBuild { dotnet restore # Use dotnet publish for .NET Core and .NET 5+ - dotnet publish --configuration Release --self-contained --framework $target $AssemblyVersion --output ../Libs/$target + dotnet publish --configuration Release --self-contained --framework $target $AssemblyVersion $AssemblyPrerelease --output ../Libs/$target if (!$?) { throw "Build failed for target framework '$target'." @@ -940,7 +955,10 @@ Add-BuildTask Default { # Synopsis: Stamps the version onto the Module Add-BuildTask StampVersion { $pwshVersions = Get-PodeBuildPwshEOL - (Get-Content ./pkg/Pode.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version -replace '\$versionsUntested\$', $pwshVersions.eol -replace '\$versionsSupported\$', $pwshVersions.supported -replace '\$buildyear\$', ((get-date).Year) } | Set-Content ./pkg/Pode.psd1 + if ($Prerelease) { + $prereleaseValue = "Prerelease = '$Prerelease'" + } + (Get-Content ./pkg/Pode.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version -replace '\$versionsUntested\$', $pwshVersions.eol -replace '\$versionsSupported\$', $pwshVersions.supported -replace '\$buildyear\$', ((get-date).Year) -replace '#\$Prerelease-Here\$', $prereleaseValue } | Set-Content ./pkg/Pode.psd1 (Get-Content ./pkg/Pode.Internal.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./pkg/Pode.Internal.psd1 (Get-Content ./packers/choco/pode_template.nuspec) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/pode.nuspec (Get-Content ./packers/choco/tools/ChocolateyInstall_template.ps1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/tools/ChocolateyInstall.ps1 @@ -1712,4 +1730,4 @@ task ReleaseNotes { $categories[$category] | Sort-Object | ForEach-Object { Write-Host $_ } Write-Host '' } -} +} \ No newline at end of file diff --git a/src/Pode.Internal.psm1 b/src/Pode.Internal.psm1 index dcb516be0..96176933a 100644 --- a/src/Pode.Internal.psm1 +++ b/src/Pode.Internal.psm1 @@ -14,4 +14,7 @@ Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::Ge $funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ } # export the module's public functions -Export-ModuleMember -Function ($funcs.Name) \ No newline at end of file +Export-ModuleMember -Function ($funcs.Name) + +# Ensure backward compatibility by creating aliases for legacy Pode OpenAPI function names. +New-PodeFunctionAlias \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index b520f389c..fa158a2ec 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -541,6 +541,9 @@ # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ PSData = @{ + + #$Prerelease-Here$ + # Tags applied to this module. These help with module discovery in online galleries. Tags = @( 'powershell', 'web', 'server', 'http', 'https', 'listener', 'rest', 'api', 'tcp', diff --git a/src/Pode.psm1 b/src/Pode.psm1 index 34a812c3a..8e117f136 100644 --- a/src/Pode.psm1 +++ b/src/Pode.psm1 @@ -75,18 +75,26 @@ try { $moduleManifestPath = Join-Path -Path $root -ChildPath 'Pode.psd1' # Import the module manifest to access its properties - $moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath -ErrorAction Stop + $PodeManifest = Import-PowerShellDataFile -Path $moduleManifestPath -ErrorAction Stop $podeDll = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Pode' } if ($podeDll) { - if ( $moduleManifest.ModuleVersion -ne '$version$') { - $moduleVersion = ([version]::new($moduleManifest.ModuleVersion + '.0')) + if ( $PodeManifest.ModuleVersion -ne '$version$') { + $moduleVersion = ([version]::new($PodeManifest.ModuleVersion + '.0')) if ($podeDll.GetName().Version -ne $moduleVersion) { # An existing incompatible Pode.DLL version {0} is loaded. Version {1} is required. Open a new Powershell/pwsh session and retry. throw ($PodeLocale.incompatiblePodeDllExceptionMessage -f $podeDll.GetName().Version, $moduleVersion) } + $assemblyInformationalVersion = $podeDll.CustomAttributes.Where({ $_.AttributeType -eq [System.Reflection.AssemblyInformationalVersionAttribute] }) + if ($null -ne $PodeManifest.PrivateData.PSData.Prerelease) { + if (! $assemblyInformationalVersion.ConstructorArguments.Value.Contains($PodeManifest.PrivateData.PSData.Prerelease)) { + throw ($PodeLocale.incompatiblePodeDllExceptionMessage -f $assemblyInformationalVersion.ConstructorArguments.Value, "$moduleVersion-$($PodeManifest.PrivateData.PSData.Prerelease)") + } + }elseif($assemblyInformationalVersion.ConstructorArguments.Value.Contains('-')){ + throw ($PodeLocale.incompatiblePodeDllExceptionMessage -f $assemblyInformationalVersion.ConstructorArguments.Value, $moduleVersion) + } } } else { @@ -123,6 +131,9 @@ try { # load public functions Get-ChildItem "$($root)/Public/*.ps1" | ForEach-Object { . ([System.IO.Path]::GetFullPath($_)) } + # Ensure backward compatibility by creating aliases for legacy Pode OpenAPI function names. + New-PodeFunctionAlias + # get functions from memory and compare to existing to find new functions added $funcs = Get-ChildItem Function: | Where-Object { $sysfuncs -notcontains $_ } $aliases = Get-ChildItem Alias: | Where-Object { $sysaliases -notcontains $_ } @@ -141,6 +152,5 @@ catch { } finally { # Cleanup temporary variables - Remove-Variable -Name 'tmpPodeLocale', 'localesPath', 'moduleManifest', 'root', 'version', 'libsPath', 'netFolder', 'podeDll', 'sysfuncs', 'sysaliases', 'funcs', 'aliases', 'moduleManifestPath', 'moduleVersion' -ErrorAction SilentlyContinue + Remove-Variable -Name 'tmpPodeLocale', 'localesPath', 'root', 'version', 'libsPath', 'netFolder', 'podeDll', 'sysfuncs', 'sysaliases', 'funcs', 'aliases', 'moduleManifestPath', 'moduleVersion' -ErrorAction SilentlyContinue } - diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 804e5e7d2..b2a32e66c 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3278,30 +3278,6 @@ function Test-PodePlaceholder { } -<# -.SYNOPSIS -Retrieves the PowerShell module manifest object for the specified module. - -.DESCRIPTION -This function constructs the path to a PowerShell module manifest file (.psd1) located in the parent directory of the script root. It then imports the module manifest file to access its properties and returns the manifest object. This can be useful for scripts that need to dynamically discover and utilize module metadata, such as version, dependencies, and exported functions. - -.PARAMETERS -This function does not accept any parameters. - -.EXAMPLE -$manifest = Get-PodeModuleManifest -This example calls the `Get-PodeModuleManifest` function to retrieve the module manifest object and stores it in the variable `$manifest`. - -#> -function Get-PodeModuleManifest { - # Construct the path to the module manifest (.psd1 file) - $moduleManifestPath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Pode.psd1' - - # Import the module manifest to access its properties - $moduleManifest = Import-PowerShellDataFile -Path $moduleManifestPath - return $moduleManifest -} - <# .SYNOPSIS Tests the running PowerShell version for compatibility with Pode, identifying end-of-life (EOL) and untested versions. @@ -3335,8 +3311,7 @@ function Test-PodeVersionPwshEOL { param( [switch] $ReportUntested ) - $moduleManifest = Get-PodeModuleManifest - if ($moduleManifest.ModuleVersion -eq '$version$') { + if ($PodeManifest.ModuleVersion -eq '$version$') { return @{ eol = $false supported = $true @@ -3344,7 +3319,7 @@ function Test-PodeVersionPwshEOL { } $psVersion = $PSVersionTable.PSVersion - $eolVersions = $moduleManifest.PrivateData.PwshVersions.Untested -split ',' + $eolVersions = $PodeManifest.PrivateData.PwshVersions.Untested -split ',' $isEol = "$($psVersion.Major).$($psVersion.Minor)" -in $eolVersions if ($isEol) { @@ -3352,7 +3327,7 @@ function Test-PodeVersionPwshEOL { Write-PodeHost ($PodeLocale.eolPowerShellWarningMessage -f $PodeVersion, $PSVersion) -ForegroundColor Yellow } - $SupportedVersions = $moduleManifest.PrivateData.PwshVersions.Supported -split ',' + $SupportedVersions = $PodeManifest.PrivateData.PwshVersions.Supported -split ',' $isSupported = "$($psVersion.Major).$($psVersion.Minor)" -in $SupportedVersions if ((! $isSupported) -and (! $isEol) -and $ReportUntested) { @@ -3966,3 +3941,44 @@ function ConvertTo-PodeSleep { function Test-PodeIsISEHost { return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name)) } + +<# +.SYNOPSIS + Creates aliases for Pode OpenAPI functions to support legacy naming conventions. +.DESCRIPTION + This function sets up the following aliases in the current script scope: + - New-PodeOASchemaProperty as an alias for New-PodeOAComponentSchemaProperty. + - Enable-PodeOpenApiViewer as an alias for Enable-PodeOAViewer. + - Enable-PodeOA as an alias for Enable-PodeOpenApi. + - Get-PodeOpenApiDefinition as an alias for Get-PodeOADefinition. + The function helps maintain backward compatibility and simplifies calling Pode OpenAPI functions. + +.PARAMETER None + This function does not accept any parameters. +.OUTPUTS + None. The function creates aliases and does not output any objects. +.EXAMPLE + PS C:\> New-PodeFunctionAlias + The function creates the necessary aliases for Pode OpenAPI functions in the current session. +.NOTES + This function is part of the Pode project and adheres to the coding standards defined in the Pode GitHub Repository. + Internal function subject to change. +#> +function New-PodeFunctionAlias { + # Alias + if (!(Test-Path Alias:New-PodeOASchemaProperty)) { + New-Alias New-PodeOASchemaProperty -Value New-PodeOAComponentSchemaProperty -Scope Script + } + + if (!(Test-Path Alias:Enable-PodeOpenApiViewer)) { + New-Alias Enable-PodeOpenApiViewer -Value Enable-PodeOAViewer -Scope Script + } + + if (!(Test-Path Alias:Enable-PodeOA)) { + New-Alias Enable-PodeOA -Value Enable-PodeOpenApi -Scope Script + } + + if (!(Test-Path Alias:Get-PodeOpenApiDefinition)) { + New-Alias Get-PodeOpenApiDefinition -Value Get-PodeOADefinition -Scope Script + } +} \ No newline at end of file diff --git a/src/Public/OAComponents.ps1 b/src/Public/OAComponents.ps1 index a829f5274..7a49d94f6 100644 --- a/src/Public/OAComponents.ps1 +++ b/src/Public/OAComponents.ps1 @@ -950,16 +950,4 @@ function Remove-PodeOAComponent { $PodeContext.Server.OpenAPI.Definitions[$tag].components[$field ].remove($Name) } } -} - -if (!(Test-Path Alias:Enable-PodeOpenApiViewer)) { - New-Alias Enable-PodeOpenApiViewer -Value Enable-PodeOAViewer -} - -if (!(Test-Path Alias:Enable-PodeOA)) { - New-Alias Enable-PodeOA -Value Enable-PodeOpenApi -} - -if (!(Test-Path Alias:Get-PodeOpenApiDefinition)) { - New-Alias Get-PodeOpenApiDefinition -Value Get-PodeOADefinition -} +} \ No newline at end of file diff --git a/src/Public/OAProperties.ps1 b/src/Public/OAProperties.ps1 index 322bc2f21..ae4adc834 100644 --- a/src/Public/OAProperties.ps1 +++ b/src/Public/OAProperties.ps1 @@ -2072,9 +2072,4 @@ function New-PodeOAComponentSchemaProperty { return $param } } -} - - -if (!(Test-Path Alias:New-PodeOASchemaProperty)) { - New-Alias New-PodeOASchemaProperty -Value New-PodeOAComponentSchemaProperty } \ No newline at end of file diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index e193bbbc3..9391e8845 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -1356,38 +1356,47 @@ function New-PodeCron { <# .SYNOPSIS -Gets the version of the Pode module. + Retrieves the version of the Pode module. .DESCRIPTION -The Get-PodeVersion function checks the version of the Pode module specified in the module manifest. If the module version is not a placeholder value ('$version$'), it returns the actual version prefixed with 'v.'. If the module version is the placeholder value, indicating the development branch, it returns '[develop branch]'. + The `Get-PodeVersion` function checks the version of the Pode module as specified in the module manifest. + If the module version is **not** the placeholder value (`'$version$'`), it returns the actual version prefixed with `'v'`. + If the module version **is** the placeholder value, indicating the development branch, it returns `"[dev]"`. -.PARAMETER None -This function does not accept any parameters. +.PARAMETER Raw + If specified, the function returns only the raw module version without the `'v'` prefix. + By default, the function formats the version as `'vX.Y.Z'` unless the module is in development mode. .OUTPUTS -System.String -Returns a string indicating the version of the Pode module or '[dev]' if on a development version. + System.String + Returns a string representing the Pode module version in one of the following formats: + - `"vX.Y.Z"` for a release version (e.g., `"v1.2.3"`). + - `"[dev]"` for development versions. .EXAMPLE -PS> $moduleManifest = @{ ModuleVersion = '1.2.3' } -PS> Get-PodeVersion - -Returns 'v1.2.3'. + PS> Get-PodeVersion + Returns the Pode module version, e.g., `'v1.2.3'` for release versions or `"[dev]"` if in development. .EXAMPLE -PS> $moduleManifest = @{ ModuleVersion = '$version$' } -PS> Get-PodeVersion - -Returns '[dev]'. + PS> Get-PodeVersion -Raw + Returns the raw version number, e.g., `'1.2.3'`, without the `'v'` prefix. .NOTES -This function assumes that $moduleManifest is a hashtable representing the loaded module manifest, with a key of ModuleVersion. - + - If the module version is a placeholder (`'$version$'`), the function assumes it's running from the development branch. #> function Get-PodeVersion { - $moduleManifest = Get-PodeModuleManifest - if ($moduleManifest.ModuleVersion -ne '$version$') { - return "v$($moduleManifest.ModuleVersion)" + param ( + [switch] + $Raw + ) + + if ($PodeManifest.ModuleVersion -ne '$version$') { + $prefix = if ($Raw) { '' } else { 'v' } + if ($PodeManifest.PrivateData.PSData.Prerelease) { + return "$prefix$($PodeManifest.ModuleVersion)-$($PodeManifest.PrivateData.PSData.Prerelease)" + } + return "$prefix$($PodeManifest.ModuleVersion)" + } else { return '[dev]' diff --git a/tests/unit/OpenApi.Tests.ps1 b/tests/unit/OpenApi.Tests.ps1 index b62b29b89..f72474521 100644 --- a/tests/unit/OpenApi.Tests.ps1 +++ b/tests/unit/OpenApi.Tests.ps1 @@ -1797,7 +1797,7 @@ Describe 'OpenApi' { } - Context 'New-PodeOASchemaProperty' { + Context 'New-PodeOAComponentSchemaProperty' { BeforeEach { Add-PodeOAComponentSchema -Name 'Cat' -Schema ( New-PodeOAObjectProperty -Properties @( @@ -1807,12 +1807,12 @@ Describe 'OpenApi' { } # Check if the function exists - It 'New-PodeOASchemaProperty function exists' { - Get-Command New-PodeOASchemaProperty | Should -Not -Be $null + It 'New-PodeOAComponentSchemaProperty function exists' { + Get-Command New-PodeOAComponentSchemaProperty | Should -Not -Be $null } It 'Standard' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Reference 'Cat' + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Reference 'Cat' $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 0 $result.type | Should -Be 'schema' @@ -1821,15 +1821,15 @@ Describe 'OpenApi' { } It 'ArrayNoSwitchesUniqueItems' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -Array -MinItems 2 -MaxItems 4 -UniqueItems + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -Array -MinItems 2 -MaxItems 4 -UniqueItems $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 1 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result.array | Should -BeTrue $result.uniqueItems | Should -BeTrue $result.minItems | Should -BeTrue @@ -1837,15 +1837,15 @@ Describe 'OpenApi' { } It 'ArrayDeprecatedUniqueItems' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4 -UniqueItems + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4 -UniqueItems $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 1 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result.deprecated | Should -Be $true $result.array | Should -BeTrue $result.uniqueItems | Should -BeTrue @@ -1853,15 +1853,15 @@ Describe 'OpenApi' { $result.maxItems | Should -BeTrue } It 'ArrayNullableUniqueItems' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4 -UniqueItems + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4 -UniqueItems $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 2 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result['nullable'] | Should -Be $true $result.array | Should -BeTrue $result.uniqueItems | Should -BeTrue @@ -1869,15 +1869,15 @@ Describe 'OpenApi' { $result.maxItems | Should -BeTrue } It 'ArrayWriteOnlyUniqueItems' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 2 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result['writeOnly'] | Should -Be $true $result.array | Should -BeTrue $result.uniqueItems | Should -BeTrue @@ -1885,15 +1885,15 @@ Describe 'OpenApi' { $result.maxItems | Should -BeTrue } It 'ArrayReadOnlyUniqueItems' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4 -UniqueItems $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 2 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result['readOnly'] | Should -Be $true $result.array | Should -BeTrue $result.uniqueItems | Should -BeTrue @@ -1902,75 +1902,75 @@ Describe 'OpenApi' { } It 'ArrayNoSwitches' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -Array -MinItems 2 -MaxItems 4 + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -Array -MinItems 2 -MaxItems 4 $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 1 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result.array | Should -BeTrue $result.minItems | Should -BeTrue $result.maxItems | Should -BeTrue } It 'ArrayDeprecated' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4 + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -Deprecated -Array -MinItems 2 -MaxItems 4 $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 1 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result.deprecated | Should -Be $true $result.array | Should -BeTrue $result.minItems | Should -BeTrue $result.maxItems | Should -BeTrue } It 'ArrayNullable' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4 + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -Nullable -Array -MinItems 2 -MaxItems 4 $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 2 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result['nullable'] | Should -Be $true $result.array | Should -BeTrue $result.minItems | Should -BeTrue $result.maxItems | Should -BeTrue } It 'ArrayWriteOnly' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4 + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -WriteOnly -Array -MinItems 2 -MaxItems 4 $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 2 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result['writeOnly'] | Should -Be $true $result.array | Should -BeTrue $result.minItems | Should -BeTrue $result.maxItems | Should -BeTrue } It 'ArrayReadOnly' { - $result = New-PodeOASchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOASchemaProperty' -Reference 'Cat' ` - -Example 'Example for New-PodeOASchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4 + $result = New-PodeOAComponentSchemaProperty -Name 'testSchema' -Description 'Test for New-PodeOAComponentSchemaProperty' -Reference 'Cat' ` + -Example 'Example for New-PodeOAComponentSchemaProperty' -ReadOnly -Array -MinItems 2 -MaxItems 4 $result | Should -Not -BeNullOrEmpty #$result.Count | Should -Be 2 $result.type | Should -Be 'schema' $result.name | Should -Be 'testSchema' $result.schema | Should -Be 'Cat' - $result.description | Should -Be 'Test for New-PodeOASchemaProperty' - $result['example'] | Should -Be 'Example for New-PodeOASchemaProperty' + $result.description | Should -Be 'Test for New-PodeOAComponentSchemaProperty' + $result['example'] | Should -Be 'Example for New-PodeOAComponentSchemaProperty' $result['readOnly'] | Should -Be $true $result.array | Should -BeTrue $result.minItems | Should -BeTrue @@ -3352,10 +3352,10 @@ Describe 'OpenApi' { $Pet = New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' -Properties ( (New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -ReadOnly ), (New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required) , - (New-PodeOASchemaProperty -Name 'category' -Reference 'Category' ), + (New-PodeOAComponentSchemaProperty -Name 'category' -Reference 'Category' ), (New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required) , (New-PodeOAStringProperty -Name 'photoUrls' -Array) , - (New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag') , + (New-PodeOAComponentSchemaProperty -Name 'tags' -Reference 'Tag') , (New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold')) ) $Pet.type | Should -be 'object' @@ -3407,10 +3407,10 @@ Describe 'OpenApi' { It 'By Pipeline' { $Pet = New-PodeOAIntProperty -Name 'id'-Format Int64 -Example 10 -ReadOnly | New-PodeOAStringProperty -Name 'name' -Example 'doggie' -Required | - New-PodeOASchemaProperty -Name 'category' -Reference 'Category' | + New-PodeOAComponentSchemaProperty -Name 'category' -Reference 'Category' | New-PodeOAStringProperty -Name 'petType' -Example 'dog' -Required | New-PodeOAStringProperty -Name 'photoUrls' -Array | - New-PodeOASchemaProperty -Name 'tags' -Reference 'Tag' | + New-PodeOAComponentSchemaProperty -Name 'tags' -Reference 'Tag' | New-PodeOAStringProperty -Name 'status' -Description 'pet status in the store' -Enum @('available', 'pending', 'sold') | New-PodeOAObjectProperty -Name 'Pet' -XmlName 'pet' $Pet.type | Should -be 'object' From 798a09869e481b33b939f8d92e4fe42210fcc867 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 3 Mar 2025 05:57:59 -0800 Subject: [PATCH 3/4] Squashed changes from PR #1509 --- docs/Tutorials/Endpoints/Basics.md | 36 ++++++++++++ src/Misc/favicon.ico | Bin 0 -> 1150 bytes src/Private/PodeServer.ps1 | 87 ++++++++++++++++------------- src/Public/Endpoint.ps1 | 49 ++++++++++++---- 4 files changed, 121 insertions(+), 51 deletions(-) create mode 100644 src/Misc/favicon.ico diff --git a/docs/Tutorials/Endpoints/Basics.md b/docs/Tutorials/Endpoints/Basics.md index 888a529ba..1060a5bb8 100644 --- a/docs/Tutorials/Endpoints/Basics.md +++ b/docs/Tutorials/Endpoints/Basics.md @@ -188,3 +188,39 @@ To set this property, include it in `server.psd1` configuration file as shown be } } ``` + +## Favicons + +Pode allows you to customize or disable the favicon for HTTP/HTTPS endpoints. By default, Pode serves a built-in `favicon.ico`, but you can override this behavior using the `-Favicon` and `-NoFavicon` parameters. + +- **`-Favicon` (byte[])**: Allows you to specify a custom favicon as a byte array. +- **`-NoFavicon` (switch)**: Disables the favicon, preventing browsers from requesting it. + +### **Favicon Format and Specifications** + +Favicons are typically stored in the `.ico` format, which is a container that can hold multiple image sizes and color depths. This ensures compatibility with different browsers and devices. Some modern browsers also support `.png` and `.svg` favicons. + +For more details on favicon formats and specifications, refer to the [Favicon specification](https://en.wikipedia.org/wiki/Favicon) and [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988). + +### **Favicon Size Recommendations** + +Favicons should include multiple resolutions for optimal display across different devices. Recommended sizes include: + +- **16x16** → Used in browser tabs, bookmarks, and address bars. +- **32x32** → Used in browser tabs on higher-resolution displays. +- **48x48** → Used by some older browsers and web applications. +- **64x64+** → Generally not used by browsers but can be helpful for scalability in web apps. +- **256x256** → Mainly for **Windows app icons** (not typically used as a favicon in browsers). + +### **Usage Example** + +```powershell +# Load a custom favicon from file +$iconBytes = [System.IO.File]::ReadAllBytes("C:\path\to\custom.ico") +Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Favicon $iconBytes + +# Disable favicon for an endpoint +Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -NoFavicon +``` + +Using a favicon enhances the user experience by providing a recognizable site icon in browser tabs and bookmarks. diff --git a/src/Misc/favicon.ico b/src/Misc/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0b937da25e5f7176dcab9c4220fe1955959f1a6e GIT binary patch literal 1150 zcmbVMJ8V*66#jYH!I-{`ZbqG~OMQ$cnz+@)#lgj-lQD62ad0&5J`!DZ@d*?tT0*&S zxfVkU4NXPSLTwvfX#o|eJU`EWE)NysAmse_{^y+Uyw4TFK>v{uLFZv{%`C(LA;d8v zVi8B@(?bZ7jqTNm?{q|zNd#;H!5K4=u|pq}mQjfD68{>VG zP{~%nYJ_SZ;+SK>vy9tG+mPRQ%?2W?vl;X1) z@F_aYI)$7IzUPGZpCanullOsE10nYq=OBGlN>ThMuOS}3L~#p_r&ifSG3Oq{ryc<9 zHv5yh)$bqJmpmHv0@)pB8};1`O1Zf~d{W}k)2o&fI=gVU>t7z1=iB-Xu_cyukvSxbxZ*;uU&p(~I$Nu8)E^?l-eqqlM?uD$cLh}?; z&v1;z{4ep-e90cIhAyDKn`Rq@tP2s}arPmx;kiS|Z$4pJvtFd$PBTW%W~4Vi0&iWz z*h<`{xvR78R^kp)t2ZczD&Mo~52+3Zx%b`q;rZ{(uWC1OoMF#V?4+%Xk;k`fm1aes zGu1|W;M8}kH~+kUaxdiGS*U&nJ*R~!d|bTD{(j#>(?9o5qdk%&>)qcOxx=0PCRum? Ky5E8Sef|Vu3g27+ literal 0 HcmV?d00001 diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index bb3356d8a..b8e6453c6 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -210,59 +210,66 @@ function Start-PodeWebServer { if ($Request.IsAborted) { throw $Request.Error } - - # 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)") - } - - 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) - } - - $WebEvent.Sse = @{ - Name = $WebEvent.Request.SseClientName - Group = $WebEvent.Request.SseClientGroup - ClientId = $WebEvent.Request.SseClientId - LastEventId = $null - IsLocal = $false - } + + # deal with favicon if available + if ($WebEvent.Path -eq '/favicon.ico' -and ($null -ne $PodeContext.Server.Endpoints[$context.EndpointName].Favicon)) { + # Write the file content as the HTTP response + Write-PodeTextResponse -Bytes $PodeContext.Server.Endpoints[$context.EndpointName].Favicon -ContentType 'image/png' -StatusCode 200 } + else { + # 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)") + } + + 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 - } } } } diff --git a/src/Public/Endpoint.ps1 b/src/Public/Endpoint.ps1 index d6b1c97c1..d2d9fa523 100644 --- a/src/Public/Endpoint.ps1 +++ b/src/Public/Endpoint.ps1 @@ -1,4 +1,3 @@ - <# .SYNOPSIS Bind an endpoint to listen for incoming Requests. @@ -19,13 +18,13 @@ An optional hostname for the endpoint, specifying a hostname restricts access to The protocol of the supplied endpoint. .PARAMETER Certificate -The path to a certificate that can be use to enable HTTPS +The path to a certificate that can be used to enable HTTPS. .PARAMETER CertificatePassword -The password for the certificate file referenced in Certificate +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 +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). @@ -34,13 +33,13 @@ A certificate thumbprint to bind onto HTTPS endpoints (Windows). 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). +The name of a certificate 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). +The location of a certificate store where a certificate can be found (Default: CurrentUser) (Windows). .PARAMETER X509Certificate -The raw X509 certificate that can be use to enable HTTPS +The raw X509 certificate that can be used to enable HTTPS. .PARAMETER TlsMode The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit). @@ -58,16 +57,16 @@ A quick description of the Endpoint - normally used in OpenAPI. 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). +One or more optional SSL Protocols this endpoint 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. +Ignore Administrator checks for non-localhost endpoints. .PARAMETER SelfSigned -Create and bind a self-signed certifcate for HTTPS endpoints. +Create and bind a self-signed certificate for HTTPS endpoints. .PARAMETER AllowClientCertificate Allow for client certificates to be sent on requests. @@ -85,6 +84,12 @@ For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 ad .PARAMETER Default If supplied, this endpoint will be the default one used for internally generating URLs. +.PARAMETER Favicon +A byte array representing a custom favicon for HTTP/HTTPS endpoints. + +.PARAMETER NoFavicon +If supplied, disables the default favicon for HTTP/HTTPS endpoints. + .EXAMPLE Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http @@ -208,7 +213,13 @@ function Add-PodeEndpoint { $DualMode, [switch] - $Default + $Default, + + [byte[]] + $Favicon, + + [switch] + $NoFavicon ) # error if serverless @@ -292,6 +303,20 @@ function Add-PodeEndpoint { throw ($PodeLocale.crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage) } + if (! $NoFavicon ) { + $Favicon = $null + } + # Load the default favicon + elseif ( ($null -eq $Favicon) -and (@('Http', 'Https') -icontains $Protocol)) { + $podeRoot = Get-PodeModuleMiscPath + if (Test-PodeIsPSCore) { + $Favicon = (Get-Content -Path ([System.IO.Path]::Combine($podeRoot, 'favicon.ico')) -Raw -AsByteStream) + } + else { + $Favicon = (Get-Content -Path ([System.IO.Path]::Combine($podeRoot, 'favicon.ico')) -Raw -Encoding byte) + } + } + # new endpoint object $obj = @{ Name = $Name @@ -324,8 +349,10 @@ function Add-PodeEndpoint { Acknowledge = $Acknowledge CRLFMessageEnd = $CRLFMessageEnd } + Favicon = $Favicon } + # set ssl protocols if (!(Test-PodeIsEmpty $SslProtocol)) { $obj.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $SslProtocol) From aa3641895d13b86851e8924c252269551e376d23 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 3 Mar 2025 06:07:03 -0800 Subject: [PATCH 4/4] Squashed commit of the following: commit f3c3aaca5a51aadec51354f662a2f2b20c7be56a Merge: 30e7223a 67505f56 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Sun Feb 23 07:30:56 2025 -0800 Merge branch 'develop' into service commit 30e7223ae93c087668dbd7b37b6c4e84ca509ffc Merge: c559b8e9 cbdc62fe Author: mdaneri Date: Sat Feb 22 09:18:44 2025 -0800 Merge remote-tracking branch 'upstream/develop' into service commit c559b8e92e35cc8024b77d0001b9e9eb1dde15b1 Merge: 4ef1b847 fbf6ecfb Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Sat Feb 22 06:33:02 2025 -0800 Merge branch 'develop' into service commit 4ef1b8471cb1dcec715f6af52474443c779c2d46 Merge: a25119b3 a76741bc Author: mdaneri Date: Sun Feb 16 07:08:44 2025 -0800 Merge remote-tracking branch 'upstream/develop' into service commit a25119b3ddf20c7b9d769569f45095719d262262 Merge: adc01aa8 a236a1a0 Author: mdaneri Date: Tue Feb 11 18:51:30 2025 -0800 Merge remote-tracking branch 'upstream/develop' into service commit adc01aa8ee07ee0b71723e72bd2767e77cba9df8 Merge: 060f313a 75e29626 Author: mdaneri Date: Sun Feb 9 07:24:08 2025 -0800 Merge remote-tracking branch 'upstream/develop' into service commit 060f313a1ebf3b0686b6c4082e2ce39e4f22c6bb Merge: 752cbabe 6b23fc33 Author: mdaneri Date: Wed Feb 5 14:35:21 2025 -0800 Merge remote-tracking branch 'upstream/develop' into service commit 752cbabe06f69f96bbdbe2d642b8747c3aadf5b7 Author: mdaneri Date: Wed Jan 29 07:16:16 2025 -0800 minor fixes commit 609d71bcce56f296846ff8a6134766e48e05529f Author: mdaneri Date: Tue Jan 28 13:40:54 2025 -0800 udate service test to include suspend commit 1c9d486fa9d489dd95d996328f490562cd4393fa Author: mdaneri Date: Tue Jan 28 07:47:49 2025 -0800 states improvement commit 5864d56378ecec9858539c3fbebd4ffe3e741dfd Author: mdaneri Date: Tue Jan 28 07:23:51 2025 -0800 enum changes commit e5317b813880fd11ebf3ec8295f29a7d6613cc25 Author: mdaneri Date: Mon Jan 27 10:23:15 2025 -0800 update commit 285026c97052cab793f8f31cd7f5717e76a00696 Author: mdaneri Date: Sun Jan 26 11:40:44 2025 -0800 Fix a resume issue commit 717c8d883ca53eea034a2e1a447d4bf767de65ee Author: mdaneri Date: Sun Jan 26 08:49:22 2025 -0800 first fixes commit 886b8a7cf580d3912d4158f71877e44d4eb433fc Merge: 5c8bb3df f4db4b62 Author: mdaneri Date: Sun Jan 26 08:20:14 2025 -0800 Merge remote-tracking branch 'upstream/develop' into service commit 5c8bb3df8d0f653190d4c1ae55b4df3615fc82ff Author: mdaneri Date: Fri Nov 29 05:28:44 2024 -0800 Fix examples commit ac4cbb9643dc1b955f632770923e12058592a449 Author: mdaneri Date: Thu Nov 28 07:25:56 2024 -0800 fix pipename generator on mac/linux+ log errors commit d2b5b49143bb988516ab93faada731ce527bccf0 Author: mdaneri Date: Thu Nov 28 06:37:06 2024 -0800 add full support for macos agents commit 1761077e5071899e8bbe5690f5fc65dc5b14e428 Author: mdaneri Date: Wed Nov 27 15:46:16 2024 -0800 Added log rotation and additional features commit 40d5bbc403fc9f2d04ebc366354340ed5ac3c9a8 Author: mdaneri Date: Wed Nov 27 14:30:29 2024 -0800 fixes for Powershell 5.1 commit d6a026f4aafbec4ef597331bf3beb2705958a843 Author: MDaneri Date: Wed Nov 27 10:25:18 2024 -0800 fix linux commit c9c84f3d3efa00d8b8150eb3025703cf42ebaaab Author: mdaneri Date: Wed Nov 27 10:10:58 2024 -0800 Code Improvements commit 0c50dd1c96378d838aca2b4a917fa9945ca2b712 Author: mdaneri Date: Tue Nov 26 21:20:12 2024 -0800 Retry commit 10a272d25033f0f77af9fc6d1be436873fea4e08 Author: mdaneri Date: Tue Nov 26 17:49:17 2024 -0800 fix MacOS test (I hope) commit 6f817e6d3429cae3e57b910a63e603b4076983bc Author: mdaneri Date: Tue Nov 26 16:17:48 2024 -0800 fixes commit 79e5b2f9b0d116176a40a5edfa9a3a8a936af4aa Author: mdaneri Date: Tue Nov 26 15:56:33 2024 -0800 fix MacOS commit bc36b0f6b8270973f31adb6683c6cbd07c67a680 Author: MDaneri Date: Mon Nov 25 21:27:32 2024 -0800 test fixes commit 035cb12f51a2ade7acad6728344e9a4626529a04 Author: MDaneri Date: Mon Nov 25 17:58:12 2024 -0800 linux fixes commit 7f77d94de61ad6afe12b2f54a035748378aaed99 Author: mdaneri Date: Mon Nov 25 17:40:39 2024 -0800 fix windows test commit 12c33a4eeb633be0b8fc9544d6dbcf529011252c Author: mdaneri Date: Mon Nov 25 14:31:28 2024 -0800 improvements to the service commands commit 50b23a3766dfb1ded65e4543369bace5e71c2d0d Merge: 6af3213c 391bdfff Author: mdaneri Date: Sun Nov 24 16:50:42 2024 -0800 Merge remote-tracking branch 'upstream/develop' into service commit 6af3213c8cdc8c86268b26bf2ec75ae4954bfff8 Author: mdaneri Date: Sat Nov 23 17:37:52 2024 -0800 tests fix commit 304457d066f45c825d26b6982f8e7e360a4b168c Author: MDaneri Date: Sat Nov 23 15:50:24 2024 -0800 modified: tests/integration/Service.Tests.ps1 commit d18b1f6065a7b8ec349a6e00a017414435195dac Author: mdaneri Date: Sat Nov 23 14:49:54 2024 -0800 Fix the macos part commit ad7a7e87d694447d261f5c96ab1798378f2ddd51 Author: MDaneri Date: Sat Nov 23 11:20:40 2024 -0800 add group adm as sudo users commit d6946f3de2e5b6b9ace50ca481d3cd428ffe2d27 Author: MDaneri Date: Sat Nov 23 11:16:41 2024 -0800 replace Test-PodeBuildIsWindows with $iswindows commit 7d208c636d97837adaf6ff12e33bcb0b0238aae3 Author: MDaneri Date: Sat Nov 23 11:11:41 2024 -0800 build improvements commit 55df3ab5fe124fb1657588faf5292039c2a894d2 Author: MDaneri Date: Sat Nov 23 10:53:44 2024 -0800 adding delays commit 1b188e67680f356fb85e0de09477e401deb5fa36 Author: MDaneri Date: Sat Nov 23 10:11:38 2024 -0800 again commit f000a9eca1d1941794ff1d81678fd2f4d9a9527e Author: MDaneri Date: Sat Nov 23 10:06:27 2024 -0800 fix trhe workflow commit bb1fbb294b3bdb72c170235371c299455179f982 Author: MDaneri Date: Sat Nov 23 10:03:58 2024 -0800 try to catch $_ -like "*##[debug]*" commit ae24dd94db8c2641998cfeb5361eaaaa47a74172 Author: MDaneri Date: Sat Nov 23 09:41:36 2024 -0800 retry commit a57638659a884e93cb52052f3913b2480023a36d Author: MDaneri Date: Sat Nov 23 09:25:10 2024 -0800 support workflow debug commit 6ba641982c801452174b69d19c669d5be8a0bcd4 Author: MDaneri Date: Sat Nov 23 09:02:22 2024 -0800 linux fixes commit 604056379145ae73c2d0b433df3be6734a47e297 Author: mdaneri Date: Sat Nov 23 07:56:55 2024 -0800 Update pode.build.ps1 commit d568de198f415a8c8984e2fb7e4a73da7c3e85f4 Merge: 770702e6 7a2cf535 Author: mdaneri Date: Sat Nov 23 07:56:52 2024 -0800 Merge remote-tracking branch 'upstream/develop' into service commit 770702e6cee7ca55c0ea83e4393fd76a34fb2a25 Author: MDaneri Date: Fri Nov 22 17:04:55 2024 -0800 Added stopping and fix suspended report on linux commit 697cd8b8988d7a97eeea8e1b397c0540a7960e9f Author: mdaneri Date: Fri Nov 22 08:34:00 2024 -0800 adding tests commit b0bd2041825943a8c73c66cb9e1ddd8992e787a5 Author: mdaneri Date: Fri Nov 22 06:30:30 2024 -0800 Update Helpers.ps1 commit ad2827263ec8e3f5aa0dc871cc3c953cbca3c7fe Merge: 3cdfb0d7 508c2da0 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Fri Nov 22 05:40:18 2024 -0800 Merge branch 'develop' into service commit 3cdfb0d7cd6c8ddf66ee9b541779663e2c5955be Author: mdaneri Date: Thu Nov 21 21:16:04 2024 -0800 windows fixes commit 5ee22257dbfbbdb086e2aad661c677d2153516cc Author: mdaneri Date: Thu Nov 21 10:14:29 2024 -0800 improvements commit 44f87686b88c07af01b88f35a05555b8bb7c157d Author: mdaneri Date: Thu Nov 21 09:45:19 2024 -0800 improve workflow commit 66e48179ea675ad6bc7c30f01180f69fc7562737 Author: mdaneri Date: Wed Nov 20 12:10:13 2024 -0800 fix Test-PodeAdminPrivilege commit 6c2b883e1fbe084882df56a726fc3fa391fddb3c Author: mdaneri Date: Wed Nov 20 11:25:50 2024 -0800 add DisableLifecycleServiceOperations to build commit cc5787e7974059a2c33182d41263f362df97245f Author: mdaneri Date: Tue Nov 19 20:43:09 2024 -0800 Mac fixes commit 0978c68160e83b9aef1e1a14a4339cc89f3bd303 Author: mdaneri Date: Tue Nov 19 14:23:43 2024 -0800 fixes commit ac2fd828e00ff624cf79792bf838e140cb646512 Author: Max Daneri Date: Tue Nov 19 17:19:58 2024 -0500 Linux fixes commit 9bb9747f30d750e8c237d2c59c563cdc4ce7ab14 Author: mdaneri Date: Tue Nov 19 12:34:34 2024 -0800 minor fixes commit d959b34e8ac438f539d5debc90be5cf9064120c6 Author: mdaneri Date: Tue Nov 19 10:16:03 2024 -0800 fixes commit 0e5765dcb72aa973715dd0816428a7a82856b3d0 Author: mdaneri Date: Mon Nov 18 18:04:40 2024 -0800 Rename folders and fix windows service credential commit ab64da5cce60f5bca01a75f3cd482b557bf859cb Author: mdaneri Date: Mon Nov 18 09:51:36 2024 -0800 add EnableTransactions to mac plist commit d4ada0f7dd4996cb156fcf67abb134cf0cc90a18 Author: mdaneri Date: Mon Nov 18 09:45:00 2024 -0800 Add restart to windows using sc control 'Hello Service2' 128 commit b04f761c515eec5d99df2fe0530d01739da356a7 Author: mdaneri Date: Mon Nov 18 07:55:08 2024 -0800 fixes commit 93ca6141555ad7150b32c166b68d5c5b50b015ba Author: mdaneri Date: Sun Nov 17 10:32:09 2024 -0800 Added comments commit 4996642393a2b65ca30184cca04c4046d3dd7df9 Author: mdaneri Date: Sun Nov 17 10:23:14 2024 -0800 improvements commit c11eb7fc4e410ce6e22ac832ecfcdd2ce69d65f3 Author: mdaneri Date: Sun Nov 17 10:09:50 2024 -0800 revert to net8 commit ebf36a2a739660a57e74372f1b59b781159aa27d Author: mdaneri Date: Sun Nov 17 09:56:45 2024 -0800 Improvements commit 87254b33a3366da35ab01cbd761ef1853e770b83 Author: mdaneri Date: Sat Nov 16 19:25:45 2024 -0800 add suspend ,resume commit 756853ecd8d3d3fbf9de7f8413a6da1a382c33ae Merge: 6cc2da7f 5a0bee12 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Wed Nov 6 05:49:17 2024 -0800 Merge branch 'develop' into service commit 6cc2da7fc2a8bf56f05fb6898f175766c215a0b4 Merge: b3579ead 62bc7052 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Sun Nov 3 13:26:46 2024 -0800 Merge branch 'develop' into service commit b3579eade84e88280a3add4fd075996ebff8c646 Merge: 96cc8a30 312654bf Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Sun Nov 3 12:40:55 2024 -0800 Merge branch 'develop' into service commit 96cc8a307d91bd71b36cdd2a072191d4e51e3209 Merge: 2e6b1532 a37f33b3 Author: mdaneri Date: Sat Nov 2 14:10:16 2024 -0700 Merge remote-tracking branch 'upstream/develop' into service commit 2e6b153285377ca144e670d5f87e3b45080f0aed Merge: 49b45c02 308035d8 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Wed Oct 30 05:35:35 2024 -0700 Merge branch 'develop' into service commit 49b45c029487596d017b705121383a43935a64a3 Merge: b113821d c47ad6f7 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Mon Oct 28 04:03:56 2024 -0700 Merge branch 'develop' into service commit b113821d8c3d96c4ce8480520de17dc7c053ab61 Merge: fc3e1c3d 09d9ad02 Author: mdaneri Date: Sun Oct 27 16:45:42 2024 -0700 Merge remote-tracking branch 'upstream/develop' into service commit fc3e1c3db9133ac88b2a55d8b51d7e41b14d19dc Author: mdaneri Date: Wed Oct 23 15:50:03 2024 -0700 MacOS improvements commit d79fe5ffc56ca4eeb07ecdb305438cf422e405e1 Merge: 78e50f27 c8e23fba Author: mdaneri Date: Wed Oct 23 12:56:02 2024 -0700 Merge remote-tracking branch 'upstream/develop' into service commit 78e50f278e3f73d71c9b6abe3507ffb07e13c953 Author: mdaneri Date: Wed Oct 23 11:15:23 2024 -0700 minor log fixex commit 88246e89eaa842f4327749eedadaaa7ff8444b2e Merge: 9326e32b 0b08d675 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Wed Oct 23 06:46:21 2024 -0700 Merge branch 'develop' into service commit 9326e32b10d37a3a3879f0b92040f04e3662daf8 Author: mdaneri Date: Tue Oct 22 15:06:05 2024 -0700 fix the example commit afcd5c1bf7125cf2fe7af800c8246c72aa70dd8a Author: Max Daneri Date: Tue Oct 22 17:57:54 2024 -0400 fixes commit a67b3c3570d45a004d56d0e9bdf8d4d6c75fd98a Author: mdaneri Date: Tue Oct 22 13:30:39 2024 -0700 fixes commit 302bb09f8110d69552bca4dfd3de9a95752d14c3 Author: mdaneri Date: Tue Oct 22 12:49:32 2024 -0700 reinstated create user commit 00c525226bb7b7ade786fb707c1c89f5d7f0de8b Author: mdaneri Date: Tue Oct 22 09:57:47 2024 -0700 remove spaces between function Register-PodeService and header commit 8a314ea045bfc852944a604d298428aa65d6e662 Author: mdaneri Date: Tue Oct 22 09:50:20 2024 -0700 Code completed commit b19643d7dfa1f52807c7420d0d69525d5ac8071f Author: mdaneri Date: Mon Oct 21 18:56:28 2024 -0700 Update Service.ps1 commit 1e0cf3b474338eb51a9933da1bcfefaf9b99a7ea Author: mdaneri Date: Mon Oct 21 18:46:03 2024 -0700 Add logs commit 5c0c33db5f5b532edaa57d35afcdb8b1f13b8165 Author: Max Daneri Date: Mon Oct 21 18:17:39 2024 -0400 fixes commit 9e76fbb736fd51009fc7861e2eeb8f6313920292 Author: mdaneri Date: Mon Oct 21 13:14:34 2024 -0700 fix service path commit 36e14cc98c7459d0db7b227939ac482b2bbf70d6 Author: Max Daneri Date: Mon Oct 21 16:13:16 2024 -0400 fix commit 734f6b34d07505f2f53a0fa72a17946eca3720c2 Author: mdaneri Date: Mon Oct 21 13:06:19 2024 -0700 fixes commit 97d8e3c2eeeb94ddffcde7027fbfea58e31aaf2b Author: Max Daneri Date: Mon Oct 21 15:12:14 2024 -0400 fix linux 1 commit 1f55857a2c0fff286cebff41b990ea5d7f3d152a Author: mdaneri Date: Mon Oct 21 08:03:04 2024 -0700 Update Pode.psd1 commit 65b28d66016f20593b2a8a21413d50e5c93a9e08 Merge: c3073d25 79ec4681 Author: mdaneri Date: Mon Oct 21 08:03:01 2024 -0700 Merge remote-tracking branch 'upstream/develop' into service commit c3073d25de46db78dc00bf58515cf1dc8d7feafa Merge: 2ca36215 04115807 Author: mdaneri Date: Sun Oct 20 09:25:44 2024 -0700 Merge remote-tracking branch 'upstream/develop' into service commit 2ca3621540471c8349e63fb918631a71d303b48d Author: mdaneri Date: Sat Oct 19 10:57:50 2024 -0700 FIx Mac service commit 9d90c78f751b73e15b0a7bb779fd1e9607d29306 Author: mdaneri Date: Sat Oct 19 09:23:11 2024 -0700 Add UAC support commit 99f510941bb78359bec1068d8b71f3060c14c400 Merge: e29752ea a1811722 Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Fri Oct 18 20:29:13 2024 -0400 Merge branch 'develop' into service commit e29752eaab9a224d948a66e177ac8c97a2e1a803 Author: Max Daneri Date: Fri Oct 18 15:58:52 2024 -0400 fixes commit 0091321c9df337221bb4a8a539869f4edbdb1c2d Author: mdaneri Date: Fri Oct 18 09:48:32 2024 -0700 Remove settingsfile with unregister commit 6c51a480e5433438594f1962c4b5c40385bddf4c Author: mdaneri Date: Fri Oct 18 08:48:28 2024 -0700 Fix tests commit b87561e559458fffca7ada7d7107159d3504f97f Author: mdaneri Date: Thu Oct 17 17:28:47 2024 -0700 Fix Windows commit 430ea27bcae47d5fc93fb74c5781b28698ea0b32 Author: mdaneri Date: Thu Oct 17 16:16:02 2024 -0700 FIx Mac service commit 3c3a5cddcf30c8087e3c467fda57d242dbe6e433 Author: mdaneri Date: Thu Oct 17 11:55:45 2024 -0700 fix Mac detection commit d60d1cc2b2e268714b180ae75b4a826982ea7571 Author: mdaneri Date: Thu Oct 17 10:21:14 2024 -0700 Update to linux, Mac commit 2791504be219ffa483b4e80589deb8b3dd4cbc8a Author: mdaneri Date: Wed Oct 16 19:50:14 2024 -0700 update commit f965cbf1bde4002615149d9aba9e3f3808ddee85 Author: mdaneri Date: Wed Oct 16 07:51:08 2024 -0700 Integrated in Pode commit d42b58dbb08677749067b183b962644a7f109421 Merge: f264e07e 696cc43a Author: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Wed Oct 16 04:01:08 2024 -0700 Merge branch 'develop' into service commit f264e07e7938c94e6643d9b0588a82570618d9e3 Author: mdaneri Date: Tue Oct 15 18:39:03 2024 -0700 First drop --- .github/workflows/ci-powershell.yml | 14 +- .github/workflows/ci-pwsh7_5.yml | 13 +- .github/workflows/ci-pwsh_lts.yml | 13 +- .github/workflows/ci-pwsh_preview.yml | 13 +- .gitignore | 2 + Pode.sln | 2 + docs/Hosting/PortsBelow1024.md | 55 + docs/Hosting/RunAsService.md | 286 ++- examples/HelloService/HelloService.ps1 | 196 ++ examples/HelloService/HelloServices.ps1 | 191 ++ examples/HelloWorld/servicesettings.json | 11 + pode.build.ps1 | 303 ++- src/Locales/ar/Pode.psd1 | 8 + src/Locales/de/Pode.psd1 | 8 + src/Locales/en-us/Pode.psd1 | 10 +- src/Locales/en/Pode.psd1 | 8 + src/Locales/es/Pode.psd1 | 8 + src/Locales/fr/Pode.psd1 | 8 + src/Locales/it/Pode.psd1 | 8 + src/Locales/ja/Pode.psd1 | 8 + src/Locales/ko/Pode.psd1 | 8 + src/Locales/nl/Pode.psd1 | 8 + src/Locales/pl/Pode.psd1 | 8 + src/Locales/pt/Pode.psd1 | 8 + src/Locales/zh/Pode.psd1 | 8 + src/Pode.psd1 | 11 + src/Pode.psm1 | 9 + src/PodeMonitor/IPausableHostedService.cs | 26 + src/PodeMonitor/PipeNameGenerator.cs | 40 + src/PodeMonitor/PodeLogLevel.cs | 15 + src/PodeMonitor/PodeMonitor.cs | 448 +++++ src/PodeMonitor/PodeMonitor.csproj | 26 + src/PodeMonitor/PodeMonitorLogger.cs | 219 +++ src/PodeMonitor/PodeMonitorMain.cs | 232 +++ src/PodeMonitor/PodeMonitorServiceState.cs | 51 + src/PodeMonitor/PodeMonitorWindowsService.cs | 135 ++ src/PodeMonitor/PodeMonitorWorker.cs | 226 +++ src/PodeMonitor/PodeMonitorWorkerOptions.cs | 109 ++ src/PodeMonitor/PodeServiceStateExtensions.cs | 46 + src/Private/Context.ps1 | 26 +- src/Private/Helpers.ps1 | 287 ++- src/Private/Logging.ps1 | 30 +- src/Private/Runspaces.ps1 | 1 - src/Private/Server.ps1 | 2 + src/Private/Service.ps1 | 1644 +++++++++++++++++ src/Public/Core.ps1 | 32 +- src/Public/Endpoint.ps1 | 2 +- src/Public/Metrics.ps1 | 15 +- src/Public/Service.ps1 | 1184 ++++++++++++ src/Public/Utilities.ps1 | 21 +- tests/integration/Service.Tests.ps1 | 107 ++ tests/unit/Context.Tests.ps1 | 6 +- tests/unit/Routes.Tests.ps1 | 2 +- tests/unit/Service.Tests.ps1 | 237 +++ 54 files changed, 6194 insertions(+), 200 deletions(-) create mode 100644 docs/Hosting/PortsBelow1024.md create mode 100644 examples/HelloService/HelloService.ps1 create mode 100644 examples/HelloService/HelloServices.ps1 create mode 100644 examples/HelloWorld/servicesettings.json create mode 100644 src/PodeMonitor/IPausableHostedService.cs create mode 100644 src/PodeMonitor/PipeNameGenerator.cs create mode 100644 src/PodeMonitor/PodeLogLevel.cs create mode 100644 src/PodeMonitor/PodeMonitor.cs create mode 100644 src/PodeMonitor/PodeMonitor.csproj create mode 100644 src/PodeMonitor/PodeMonitorLogger.cs create mode 100644 src/PodeMonitor/PodeMonitorMain.cs create mode 100644 src/PodeMonitor/PodeMonitorServiceState.cs create mode 100644 src/PodeMonitor/PodeMonitorWindowsService.cs create mode 100644 src/PodeMonitor/PodeMonitorWorker.cs create mode 100644 src/PodeMonitor/PodeMonitorWorkerOptions.cs create mode 100644 src/PodeMonitor/PodeServiceStateExtensions.cs create mode 100644 src/Private/Service.ps1 create mode 100644 src/Public/Service.ps1 create mode 100644 tests/integration/Service.Tests.ps1 create mode 100644 tests/unit/Service.Tests.ps1 diff --git a/.github/workflows/ci-powershell.yml b/.github/workflows/ci-powershell.yml index adc4cd1f2..900d1806b 100644 --- a/.github/workflows/ci-powershell.yml +++ b/.github/workflows/ci-powershell.yml @@ -51,8 +51,18 @@ jobs: - name: Run Pester Tests shell: powershell run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-Build Test + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false + } + + if ($debug) { + Invoke-Build Test -PesterVerbosity Diagnostic + } else { + Invoke-Build Test + } - name: Build Packages shell: powershell diff --git a/.github/workflows/ci-pwsh7_5.yml b/.github/workflows/ci-pwsh7_5.yml index 1bf1704ea..9b0362d73 100644 --- a/.github/workflows/ci-pwsh7_5.yml +++ b/.github/workflows/ci-pwsh7_5.yml @@ -72,7 +72,18 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Build Test + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false + } + + if ($debug) { + Invoke-Build Test -PesterVerbosity Diagnostic + } else { + Invoke-Build Test + } - name: Build Packages shell: pwsh diff --git a/.github/workflows/ci-pwsh_lts.yml b/.github/workflows/ci-pwsh_lts.yml index 70ab3d0d3..de2e79856 100644 --- a/.github/workflows/ci-pwsh_lts.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -71,7 +71,18 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Build Test + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false + } + + if ($debug) { + Invoke-Build Test -PesterVerbosity Diagnostic + } else { + Invoke-Build Test + } - name: Build Packages shell: pwsh diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml index 43198540f..60c2e9c0d 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -71,7 +71,18 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Build Test + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false + } + + if ($debug) { + Invoke-Build Test -PesterVerbosity Diagnostic + } else { + Invoke-Build Test + } - name: Build Packages shell: pwsh diff --git a/.gitignore b/.gitignore index 6938c5f15..f4c715d1b 100644 --- a/.gitignore +++ b/.gitignore @@ -266,6 +266,8 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 docs/Getting-Started/Samples.md +examples/HelloService/*_svcsettings.json +examples/HelloService/svc_settings # Dump Folder Dump diff --git a/Pode.sln b/Pode.sln index 66eb3805a..02438e28a 100644 --- a/Pode.sln +++ b/Pode.sln @@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{41F81369-868 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pode", "src\Listener\Pode.csproj", "{772D5C9F-1B25-46A7-8977-412A5F7F77D1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PodeMonitor", "src\PodeMonitor\PodeMonitor.csproj", "{A927D6A5-A2AC-471A-9ABA-45916B597EB6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/docs/Hosting/PortsBelow1024.md b/docs/Hosting/PortsBelow1024.md new file mode 100644 index 000000000..c5a1c31fa --- /dev/null +++ b/docs/Hosting/PortsBelow1024.md @@ -0,0 +1,55 @@ +# Using Ports Below 1024 + +#### Introduction + +Traditionally in Linux, binding to ports below 1024 requires root privileges. This is a security measure, as these low-numbered ports are considered privileged. However, running applications as the root user poses significant security risks. This article explores methods to use these privileged ports with PowerShell (`pwsh`) in Linux, without running it as the root user. +There are different methods to achieve the goals. +Reverse Proxy is the right approach for a production environment, primarily if the server is connected directly to the internet. +The other solutions are reasonable after an in-depth risk analysis. + +#### Using a Reverse Proxy + +A reverse proxy like Nginx can listen on the privileged port and forward requests to your application running on an unprivileged port. + +**Configuration:** + +* Configure Nginx to listen on port 443 and forward requests to the port where your PowerShell script is listening. +* This method is widely used in web applications for its additional benefits like load balancing and SSL termination. + +#### iptables Redirection + +Using iptables, you can redirect traffic from a privileged port to a higher, unprivileged port. + +**Implementation:** + +* Set up an iptables rule to redirect traffic from, say, port 443 to a higher port where your PowerShell script is listening. +* `sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080` + +**Benefits:** + +* This approach doesn't require changing the privileges of the PowerShell executable or script. + +#### Using `setcap` Command + +The `setcap` utility can grant specific capabilities to an executable, like `pwsh`, enabling it to bind to privileged ports. + +**How it Works:** + +* Run `sudo setcap 'cap_net_bind_service=+ep' $(which pwsh)`. This command sets the `CAP_NET_BIND_SERVICE` capability on the PowerShell executable, allowing it to bind to any port below 1024. + +**Security Consideration:** + +* This method enhances security by avoiding running PowerShell as root, but it still grants significant privileges to the PowerShell process. + +#### Utilizing Authbind + +Authbind is a tool that allows a non-root user to bind to privileged ports. + +**Setup:** + +* Install Authbind, configure it to allow the desired port, and then start your PowerShell script using Authbind. +* For instance, `authbind --deep pwsh yourscript.ps1` allows the script to bind to a privileged port. + +**Advantages:** + +* It provides a finer-grained control over port access and doesn't require setting special capabilities on the PowerShell binary itself. diff --git a/docs/Hosting/RunAsService.md b/docs/Hosting/RunAsService.md index 2872e7d0f..734a7fbb8 100644 --- a/docs/Hosting/RunAsService.md +++ b/docs/Hosting/RunAsService.md @@ -1,141 +1,275 @@ -# Service +# Using Pode as a Service -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. +Pode provides built-in functions to easily manage services across platforms (Windows, Linux, macOS). These functions allow you to register, start, stop, suspend, resume, query, and unregister Pode services in a cross-platform way. -!!! 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 +## Registering a Service -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: +The `Register-PodeService` function creates the necessary service files and configurations for your system. + +#### Example: ```powershell -choco install nssm -y +Register-PodeService -Name "HelloService" -Description "Example Pode Service" -ParameterString "-Verbose" -Start ``` -Once installed, you'll need to set the location of the `pwsh` or `powershell` executables as a variable: +This registers a service named "HelloService" and starts it immediately after registration. The service runs your Pode script with the specified parameters. -```powershell -$exe = (Get-Command pwsh.exe).Source +### `Register-PodeService` Parameters -# or +The `Register-PodeService` function offers several parameters to customize your service registration: -$exe = (Get-Command powershell.exe).Source -``` +- **`-Name`** *(string)*: + The name of the service to register. + **Mandatory**. + +- **`-Description`** *(string)*: + A brief description of the service. Defaults to `"This is a Pode service."`. + +- **`-DisplayName`** *(string)*: + The display name for the service (Windows only). Defaults to `"Pode Service($Name)"`. + +- **`-StartupType`** *(string)*: + Specifies the startup type of the service (`'Automatic'` or `'Manual'`). Defaults to `'Automatic'`. + +- **`-ParameterString`** *(string)*: + Additional parameters to pass to the worker script when the service is run. Defaults to an empty string. + +- **`-LogServicePodeHost`** *(switch)*: + Enables logging for the Pode service host. + +- **`-ShutdownWaitTimeMs`** *(int)*: + Maximum time in milliseconds to wait for the service to shut down gracefully before forcing termination. Defaults to `30,000 ms`. + +- **`-StartMaxRetryCount`** *(int)*: + Maximum number of retries to start the PowerShell process before giving up. Defaults to `3`. + +- **`-StartRetryDelayMs`** *(int)*: + Delay in milliseconds between retry attempts to start the PowerShell process. Defaults to `5,000 ms`. + +- **`-WindowsUser`** *(string)*: + Specifies the username under which the service will run. Defaults to the current user (Windows only). + +- **`-LinuxUser`** *(string)*: + Specifies the username under which the service will run. Defaults to the current user (Linux Only). + +- **`-Agent`** *(switch)*: + Create an Agent instead of a Daemon in MacOS (MacOS Only). -Next, define the name of the Windows service; as well as the full file path to your Pode server script, and the arguments to be supplied to PowerShell: +- **`-Start`** *(switch)*: + Starts the service immediately after registration. + +- **`-Password`** *(securestring)*: + A secure password for the service account (Windows only). If omitted, the service account will be `'NT AUTHORITY\SYSTEM'`. + +- **`-SecurityDescriptorSddl`** *(string)*: + A security descriptor in SDDL format specifying the permissions for the service (Windows only). + +- **`-SettingsPath`** *(string)*: + Directory to store the service configuration file (`_svcsettings.json`). Defaults to a directory under the script path. + +- **`-LogPath`** *(string)*: + Path for the service log files. Defaults to a directory under the script path. + +--- + +## Starting a Service + +You can start a registered service using the `Start-PodeService` function. + +#### Example: ```powershell -$name = 'Pode Web Server' -$file = 'C:\Pode\Server.ps1' -$arg = "-ExecutionPolicy Bypass -NoProfile -Command `"$($file)`"" +Start-PodeService -Name "HelloService" ``` -Finally, install and start the service: +This returns `$true` if the service starts successfully, `$false` otherwise. + +--- + +## Stopping a Service + +To stop a running service, use the `Stop-PodeService` function. + +#### Example: ```powershell -nssm install $name $exe $arg -nssm start $name +Stop-PodeService -Name "HelloService" ``` -!!! info - You can now navigate to your server, ie: `http://localhost:8080`. +This returns `$true` if the service stops successfully, `$false` otherwise. + +--- + +## Suspending a Service -To stop (or remove) the service afterwards, you can use the following: +Suspend a running service (Windows only) with the `Suspend-PodeService` function. + +#### Example: ```powershell -nssm stop $name -nssm remove $name confirm +Suspend-PodeService -Name "HelloService" ``` -## Linux +This pauses the service, returning `$true` if successful. + +--- + +## Resuming a Service + +Resume a suspended service (Windows only) using the `Resume-PodeService` function. -To run your Pode server as a Linux service you just need to create a `.service` file at `/etc/systemd/system`. The following is example content for an example `pode-server.service` file, which run PowerShell Core (`pwsh`), as well as you script: +#### Example: -```bash -sudo vim /etc/systemd/system/pode-server.service +```powershell +Resume-PodeService -Name "HelloService" ``` -```bash -[Unit] -Description=Pode Web Server -After=network.target +This resumes the service, returning `$true` if successful. + +--- + +## Querying a Service -[Service] -ExecStart=/usr/bin/pwsh -c /usr/src/pode/server.ps1 -nop -ep Bypass -Restart=always +To check the status of a service, use the `Get-PodeService` function. -[Install] -WantedBy=multi-user.target -Alias=pode-server.service +#### Example: + +```powershell +Get-PodeService -Name "HelloService" ``` -Finally, start the service: +This returns a hashtable with the service details: + +```powershell +Name Value +---- ----- +Status Running +Pid 17576 +Name HelloService +Sudo True +``` + +--- + +## Restarting a Service + +Restart a running service using the `Restart-PodeService` function. + +#### Example: ```powershell -sudo systemctl start pode-server +Restart-PodeService -Name "HelloService" ``` -!!! info - You can now navigate to your server, ie: `http://localhost:8080`. +This stops and starts the service, returning `$true` if successful. + +--- + +## Unregistering a Service -To stop the service afterwards, you can use the following: +When you no longer need a service, unregister it with the `Unregister-PodeService` function. + +#### Example: ```powershell -sudo systemctl stop pode-server +Unregister-PodeService -Name "HelloService" -Force ``` -### Using Ports Below 1024 -#### Introduction +This forcefully stops and removes the service, returning `$true` if successful. + +--- + +## Alternative Methods for Windows and Linux + +If the Pode functions are unavailable or you prefer manual management, you can use traditional methods to configure Pode as a service. + +### Windows (NSSM) + +To use NSSM for Pode as a Windows service: + +1. Install NSSM using Chocolatey: + + ```powershell + choco install nssm -y + ``` + +2. Configure the service: + + ```powershell + $exe = (Get-Command pwsh.exe).Source + $name = 'Pode Web Server' + $file = 'C:\Pode\Server.ps1' + $arg = "-ExecutionPolicy Bypass -NoProfile -Command `"$($file)`"" + nssm install $name $exe $arg + nssm start $name + ``` -Traditionally in Linux, binding to ports below 1024 requires root privileges. This is a security measure, as these low-numbered ports are considered privileged. However, running applications as the root user poses significant security risks. This article explores methods to use these privileged ports with PowerShell (`pwsh`) in Linux, without running it as the root user. -There are different methods to achieve the goals. -Reverse Proxy is the right approach for a production environment, primarily if the server is connected directly to the internet. -The other solutions are reasonable after an in-depth risk analysis. +3. Stop or remove the service: -#### Using a Reverse Proxy + ```powershell + nssm stop $name + nssm remove $name confirm + ``` -A reverse proxy like Nginx can listen on the privileged port and forward requests to your application running on an unprivileged port. +--- -**Configuration:** +### Linux (systemd) -* Configure Nginx to listen on port 443 and forward requests to the port where your PowerShell script is listening. -* This method is widely used in web applications for its additional benefits like load balancing and SSL termination. +To configure Pode as a Linux service: -#### iptables Redirection +1. Create a service file: -Using iptables, you can redirect traffic from a privileged port to a higher, unprivileged port. + ```bash + sudo vim /etc/systemd/system/pode-server.service + ``` -**Implementation:** +2. Add the following configuration: -* Set up an iptables rule to redirect traffic from, say, port 443 to a higher port where your PowerShell script is listening. -* `sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080` + ```bash + [Unit] + Description=Pode Web Server + After=network.target -**Benefits:** + [Service] + ExecStart=/usr/bin/pwsh -c /usr/src/pode/server.ps1 -nop -ep Bypass + Restart=always -* This approach doesn't require changing the privileges of the PowerShell executable or script. + [Install] + WantedBy=multi-user.target + Alias=pode-server.service + ``` -#### Using `setcap` Command +3. Start and stop the service: -The `setcap` utility can grant specific capabilities to an executable, like `pwsh`, enabling it to bind to privileged ports. + ```bash + sudo systemctl start pode-server + sudo systemctl stop pode-server + ``` -**How it Works:** +--- -* Run `sudo setcap 'cap_net_bind_service=+ep' $(which pwsh)`. This command sets the `CAP_NET_BIND_SERVICE` capability on the PowerShell executable, allowing it to bind to any port below 1024. +## Using Ports Below 1024 -**Security Consideration:** +For privileged ports, consider: -* This method enhances security by avoiding running PowerShell as root, but it still grants significant privileges to the PowerShell process. +1. **Reverse Proxy:** Use Nginx to forward traffic from port 443 to an unprivileged port. -#### Utilizing Authbind +2. **iptables Redirection:** Redirect port 443 to an unprivileged port: -Authbind is a tool that allows a non-root user to bind to privileged ports. + ```bash + sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8080 + ``` -**Setup:** +3. **setcap Command:** Grant PowerShell permission to bind privileged ports: -* Install Authbind, configure it to allow the desired port, and then start your PowerShell script using Authbind. -* For instance, `authbind --deep pwsh yourscript.ps1` allows the script to bind to a privileged port. + ```bash + sudo setcap 'cap_net_bind_service=+ep' $(which pwsh) + ``` -**Advantages:** +4. **Authbind:** Configure Authbind to allow binding to privileged ports: -* It provides a finer-grained control over port access and doesn't require setting special capabilities on the PowerShell binary itself. + ```bash + authbind --deep pwsh yourscript.ps1 + ``` diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 new file mode 100644 index 000000000..678c8bb99 --- /dev/null +++ b/examples/HelloService/HelloService.ps1 @@ -0,0 +1,196 @@ +<# +.SYNOPSIS + PowerShell script to register, manage, and set up a Pode service named '$ServiceName'. + +.DESCRIPTION + This script provides commands to register, start, stop, query, suspend, resume, restart, and unregister a Pode service named '$ServiceName'. + It also sets up a Pode server that listens on the specified port (default 8080) and includes a basic GET route that responds with 'Hello, Service!'. + + The script checks if the Pode module exists locally and imports it; otherwise, it imports Pode from the system. + + To test the Pode server's HTTP endpoint: + Invoke-RestMethod -Uri http://localhost:8080/ -Method Get + # Response: 'Hello, Service!' + +.PARAMETER ServiceName + Name of the service to register (Default 'Hello Service'). + +.PARAMETER Register + Registers the $ServiceName with Pode. + +.PARAMETER Password + A secure password for the service account (Windows only). If omitted, the service account will be 'NT AUTHORITY\SYSTEM'. + +.PARAMETER Agent + Defines the service as an Agent instead of a Daemon.(macOS only) + +.PARAMETER Unregister + Unregisters the $ServiceName from Pode. Use with the -Force switch to forcefully unregister the service. + +.PARAMETER Force + Used with the -Unregister parameter to forcefully unregister the service. + +.PARAMETER Start + Starts the $ServiceName. + +.PARAMETER Stop + Stops the $ServiceName. + +.PARAMETER Query + Queries the status of the $ServiceName. + +.PARAMETER Suspend + Suspends the $ServiceName. + +.PARAMETER Resume + Resumes the $ServiceName. + +.PARAMETER Restart + Restarts the $ServiceName. + +.EXAMPLE + Register the service: + ./HelloService.ps1 -Register + +.EXAMPLE + Start the service: + ./HelloService.ps1 -Start + +.EXAMPLE + Query the service: + ./HelloService.ps1 -Query + +.EXAMPLE + Stop the service: + ./HelloService.ps1 -Stop + +.EXAMPLE + Unregister the service: + ./HelloService.ps1 -Unregister -Force + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloService.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +[CmdletBinding(DefaultParameterSetName = 'Inbuilt')] +param( + [Parameter( ParameterSetName = 'Inbuilt')] + [int] + $Port = 8080, + + [Parameter( ParameterSetName = 'Inbuilt')] + [string] + $ServiceName = 'Hello Service', + + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] + [switch] + $Register, + + [Parameter(Mandatory = $false, ParameterSetName = 'Register', ValueFromPipeline = $true )] + [securestring] + $Password, + + [switch] + $Agent, + + [Parameter(Mandatory = $true, ParameterSetName = 'Unregister')] + [switch] + $Unregister, + + [Parameter( ParameterSetName = 'Unregister')] + [switch] + $Force, + + [Parameter( ParameterSetName = 'Start')] + [switch] + $Start, + + [Parameter( ParameterSetName = 'Stop')] + [switch] + $Stop, + + [Parameter( ParameterSetName = 'Query')] + [switch] + $Query, + + [Parameter( ParameterSetName = 'Suspend')] + [switch] + $Suspend, + + [Parameter( ParameterSetName = 'Resume')] + [switch] + $Resume, + + [Parameter( ParameterSetName = 'Restart')] + [switch] + $Restart +) +try { + # Get the path of the script being executed + $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)) + # Get the parent directory of the script's path + $podePath = Split-Path -Parent -Path $ScriptPath + + # Check if the Pode module file exists in the specified path + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + # If the Pode module file exists, import it + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + # If the Pode module file does not exist, import the Pode module from the system + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { + # If there is any error during the module import, throw the error + throw +} + + +if ( $Register.IsPresent) { + return Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:($Agent.IsPresent) +} +if ( $Unregister.IsPresent) { + return Unregister-PodeService -Name $ServiceName -Force:$Force -Agent:($Agent.IsPresent) +} +if ($Start.IsPresent) { + return Start-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) +} + +if ($Stop.IsPresent) { + return Stop-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) +} + +if ($Suspend.IsPresent) { + return Suspend-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) +} + +if ($Resume.IsPresent) { + return Resume-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) +} + +if ($Query.IsPresent) { + return Get-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) +} + +if ($Restart.IsPresent) { + return Restart-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) +} + +# Start the Pode server +Start-PodeServer { + New-PodeLoggingMethod -File -Name 'errors' -MaxDays 4 -Path './logs' | Enable-PodeErrorLogging -Levels Informational + + # Add an HTTP endpoint listening on localhost at port 8080 + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http + + # Add a route for GET requests to the root path '/' + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # Send a text response with 'Hello, world!' + Write-PodeTextResponse -Value 'Hello, Service!' + } +} diff --git a/examples/HelloService/HelloServices.ps1 b/examples/HelloService/HelloServices.ps1 new file mode 100644 index 000000000..94b9730c3 --- /dev/null +++ b/examples/HelloService/HelloServices.ps1 @@ -0,0 +1,191 @@ +<# +.SYNOPSIS + Script to manage multiple Pode services and set up a basic Pode server. + +.DESCRIPTION + This script registers, starts, stops, queries, and unregisters multiple Pode services based on the specified hashtable. + Additionally, it sets up a Pode server that listens on a defined port and includes routes to handle incoming HTTP requests. + + The script checks if the Pode module exists in the local path and imports it; otherwise, it uses the system-wide Pode module. + +.PARAMETER Register + Registers all services specified in the hashtable. + +.PARAMETER Password + A secure password for the service account (Windows only). If omitted, the service account will be 'NT AUTHORITY\SYSTEM'. + +.PARAMETER Agent + Defines the service as an Agent instead of a Daemon.(macOS only) + +.PARAMETER Unregister + Unregisters all services specified in the hashtable. Use with -Force to force unregistration. + +.PARAMETER Force + Forces unregistration when used with the -Unregister parameter. + +.PARAMETER Start + Starts all services specified in the hashtable. + +.PARAMETER Stop + Stops all services specified in the hashtable. + +.PARAMETER Query + Queries the status of all services specified in the hashtable. + +.PARAMETER Suspend + Suspend the 'Hello Service'. + +.PARAMETER Resume + Resume the 'Hello Service'. + +.PARAMETER Restart + Restart the 'Hello Service'. + +.EXAMPLE + Register all services: + ./HelloServices.ps1 -Register + +.EXAMPLE + Start all services: + ./HelloServices.ps1 -Start + +.EXAMPLE + Query the status of all services: + ./HelloServices.ps1 -Query + +.EXAMPLE + Stop all services: + ./HelloServices.ps1 -Stop + +.EXAMPLE + Forcefully unregister all services: + ./HelloServices.ps1 -Unregister -Force + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloServices.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + + +[CmdletBinding(DefaultParameterSetName = 'Inbuilt')] +param( + [Parameter( ParameterSetName = 'Inbuilt')] + [int] + $Port = 8080, + + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] + [switch] + $Register, + + [Parameter(Mandatory = $false, ParameterSetName = 'Register', ValueFromPipeline = $true )] + [securestring] + $Password, + + [switch] + $Agent, + + [Parameter(Mandatory = $true, ParameterSetName = 'Unregister')] + [switch] + $Unregister, + + [Parameter( ParameterSetName = 'Unregister')] + [switch] + $Force, + + [Parameter( ParameterSetName = 'Start')] + [switch] + $Start, + + [Parameter( ParameterSetName = 'Stop')] + [switch] + $Stop, + + [Parameter( ParameterSetName = 'Query')] + [switch] + $Query, + + [Parameter( ParameterSetName = 'Suspend')] + [switch] + $Suspend, + + [Parameter( ParameterSetName = 'Resume')] + [switch] + $Resume, + + [Parameter( ParameterSetName = 'Restart')] + [switch] + $Restart +) +try { + # Get the path of the script being executed + $ScriptPath = (Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)) + # Get the parent directory of the script's path + $podePath = Split-Path -Parent -Path $ScriptPath + + # Check if the Pode module file exists in the specified path + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + # If the Pode module file exists, import it + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + # If the Pode module file does not exist, import the Pode module from the system + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { + # If there is any error during the module import, throw the error + throw +} +$services = @{ + 'HelloService1' = 8081 + 'HelloService2' = 8082 + 'HelloService3' = 8083 +} + +if ( $Register.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { Register-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) -ParameterString "-Port $($_.Value)" -Password $Password } +} +if ( $Unregister.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { try { Unregister-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) -Force:$Force }catch { Write-Error -Exception $_.Exception } } + +} +if ($Start.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { Start-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } +} + +if ($Stop.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { Stop-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } +} + +if ($Query.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } +} + +if ($Resume.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { Resume-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } +} + +if ($Query.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } +} + +if ($Restart.IsPresent) { + return $services.GetEnumerator() | ForEach-Object { Restart-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } +} + +# Start the Pode server +Start-PodeServer { + New-PodeLoggingMethod -File -Name "errors-$port" -MaxDays 4 -Path './logs' | Enable-PodeErrorLogging -Levels Informational + + # Add an HTTP endpoint listening on localhost at port 8080 + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http + + # Add a route for GET requests to the root path '/' + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # Send a text response with 'Hello, world!' + Write-PodeTextResponse -Value 'Hello, Service!' + } +} diff --git a/examples/HelloWorld/servicesettings.json b/examples/HelloWorld/servicesettings.json new file mode 100644 index 000000000..881b69b12 --- /dev/null +++ b/examples/HelloWorld/servicesettings.json @@ -0,0 +1,11 @@ +{ + "PodeMonitorWorker ": { + "ScriptPath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloWorld\\HelloWorld.ps1", + "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + "ParameterString": "", + "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloWorld\\logs\\PodeMonitorService.Prod.log", + "Quiet": true, + "DisableTermination": true, + "ShutdownWaitTimeMs": 30000 + } +} \ No newline at end of file diff --git a/pode.build.ps1 b/pode.build.ps1 index 291e34892..b1a4201ed 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -110,6 +110,9 @@ param( [string] $UICulture = 'en-US', + [switch] + $DisableLifecycleServiceOperations, + [string[]] [ValidateSet('netstandard2.0', 'net8.0', 'net9.0', 'net10.0')] $TargetFrameworks = @('netstandard2.0', 'net8.0', 'net9.0'), @@ -134,28 +137,51 @@ $Versions = @{ <# .SYNOPSIS - Checks if the current environment is running on Windows. + Installs a specified package using the appropriate package manager for the OS. .DESCRIPTION - This function determines if the current PowerShell session is running on Windows. - It inspects `$PSVersionTable.Platform` and `$PSVersionTable.PSEdition` to verify the OS, - returning `$true` for Windows and `$false` for other platforms. + This function installs a specified package at a given version using platform-specific + package managers. For Windows, it uses Chocolatey (`choco`). On Unix-based systems, + it checks for `brew`, `apt-get`, and `yum` to handle installations. The function sets + the security protocol to TLS 1.2 to ensure secure connections during the installation. + +.PARAMETER name + The name of the package to install (e.g., 'git'). + +.PARAMETER version + The version of the package to install, required only for Chocolatey on Windows. .OUTPUTS - [bool] - Returns `$true` if the current environment is Windows, otherwise `$false`. + None. .EXAMPLE - if (Test-PodeBuildIsWindows) { - Write-Host "This script is running on Windows." - } + Invoke-PodeBuildInstall -Name 'git' -Version '2.30.0' + # Installs version 2.30.0 of Git on Windows if Chocolatey is available. .NOTES - - Useful for cross-platform scripts to conditionally execute Windows-specific commands. - - The `$PSVersionTable.Platform` variable may be `$null` in certain cases, so `$PSEdition` is used as an additional check. + - Requires administrator or sudo privileges on Unix-based systems. + - This function supports package installation on both Windows and Unix-based systems. + - If `choco` is available, it will use `choco` for Windows, and `brew`, `apt-get`, or `yum` for Unix-based systems. #> -function Test-PodeBuildIsWindows { - $v = $PSVersionTable - return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop')) +function Invoke-PodeBuildInstall($name, $version) { + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + + if (Test-PodeBuildIsWindows) { + if (Test-PodeBuildCommand 'choco') { + choco install $name --version $version -y --no-progress + } + } + else { + if (Test-PodeBuildCommand 'brew') { + brew install $name + } + elseif (Test-PodeBuildCommand 'apt-get') { + sudo apt-get install $name -y + } + elseif (Test-PodeBuildCommand 'yum') { + sudo yum install $name -y + } + } } <# @@ -272,76 +298,54 @@ function Test-PodeBuildCommand($cmd) { <# .SYNOPSIS - Retrieves the branch name from the GitHub Actions environment variable. + Checks if the current environment is running on Windows. .DESCRIPTION - This function extracts the branch name from the `GITHUB_REF` environment variable, - which is commonly set in GitHub Actions workflows. It removes the 'refs/heads/' prefix - from the branch reference, leaving only the branch name. + This function determines if the current PowerShell session is running on Windows. + It inspects `$PSVersionTable.Platform` and `$PSVersionTable.PSEdition` to verify the OS, + returning `$true` for Windows and `$false` for other platforms. .OUTPUTS - [string] - The name of the GitHub branch. + [bool] - Returns `$true` if the current environment is Windows, otherwise `$false`. .EXAMPLE - $branch = Get-PodeBuildBranch - Write-Host "Current branch: $branch" - # Output example: Current branch: main + if (Test-PodeBuildIsWindows) { + Write-Host "This script is running on Windows." + } .NOTES - - Only relevant in environments where `GITHUB_REF` is defined (e.g., GitHub Actions). - - Returns an empty string if `GITHUB_REF` is not set. + - Useful for cross-platform scripts to conditionally execute Windows-specific commands. + - The `$PSVersionTable.Platform` variable may be `$null` in certain cases, so `$PSEdition` is used as an additional check. #> -function Get-PodeBuildBranch { - return ($env:GITHUB_REF -ireplace 'refs\/heads\/', '') +function Test-PodeBuildIsWindows { + $v = $PSVersionTable + return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop')) } + <# .SYNOPSIS - Installs a specified package using the appropriate package manager for the OS. + Retrieves the branch name from the GitHub Actions environment variable. .DESCRIPTION - This function installs a specified package at a given version using platform-specific - package managers. For Windows, it uses Chocolatey (`choco`). On Unix-based systems, - it checks for `brew`, `apt-get`, and `yum` to handle installations. The function sets - the security protocol to TLS 1.2 to ensure secure connections during the installation. - -.PARAMETER name - The name of the package to install (e.g., 'git'). - -.PARAMETER version - The version of the package to install, required only for Chocolatey on Windows. + This function extracts the branch name from the `GITHUB_REF` environment variable, + which is commonly set in GitHub Actions workflows. It removes the 'refs/heads/' prefix + from the branch reference, leaving only the branch name. .OUTPUTS - None. + [string] - The name of the GitHub branch. .EXAMPLE - Invoke-PodeBuildInstall -Name 'git' -Version '2.30.0' - # Installs version 2.30.0 of Git on Windows if Chocolatey is available. + $branch = Get-PodeBuildBranch + Write-Host "Current branch: $branch" + # Output example: Current branch: main .NOTES - - Requires administrator or sudo privileges on Unix-based systems. - - This function supports package installation on both Windows and Unix-based systems. - - If `choco` is available, it will use `choco` for Windows, and `brew`, `apt-get`, or `yum` for Unix-based systems. + - Only relevant in environments where `GITHUB_REF` is defined (e.g., GitHub Actions). + - Returns an empty string if `GITHUB_REF` is not set. #> -function Invoke-PodeBuildInstall($name, $version) { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - - if (Test-PodeBuildIsWindows) { - if (Test-PodeBuildCommand 'choco') { - choco install $name --version $version -y --no-progress - } - } - else { - if (Test-PodeBuildCommand 'brew') { - brew install $name - } - elseif (Test-PodeBuildCommand 'apt-get') { - sudo apt-get install $name -y - } - elseif (Test-PodeBuildCommand 'yum') { - sudo yum install $name -y - } - } +function Get-PodeBuildBranch { + return ($env:GITHUB_REF -ireplace 'refs\/heads\/', '') } <# @@ -514,11 +518,104 @@ function Invoke-PodeBuildDotnetBuild { # Use dotnet publish for .NET Core and .NET 5+ dotnet publish --configuration Release --self-contained --framework $target $AssemblyVersion $AssemblyPrerelease --output ../Libs/$target + # Throw an error if the build fails if (!$?) { throw "Build failed for target framework '$target'." } } +<# +.SYNOPSIS + Builds the Pode Monitor Service for multiple target platforms using .NET SDK. + +.DESCRIPTION + This function automates the build process for the Pode Monitor Service. It: + - Determines the highest installed .NET SDK version. + - Verifies compatibility with the required SDK version. + - Optionally sets an assembly version during the build. + - Builds the service for specified runtime targets across platforms (Windows, Linux, macOS). + - Allows defining custom constants for conditional compilation. + +.PARAMETER Version + Specifies the assembly version to use for the build. If not provided, no version is set. + +.PARAMETER DisableLifecycleServiceOperations + If specified, excludes lifecycle service operations during the build by omitting related compilation constants. + +.INPUTS + None. The function does not accept pipeline input. + +.OUTPUTS + None. The function produces build artifacts in the output directory. + +.NOTES + This function is designed to work with .NET SDK and assumes it is installed and configured properly. + It throws an error if the build process fails for any target. + +.EXAMPLE + Invoke-PodeBuildDotnetMonitorSrvBuild -Version "1.0.0" + + Builds the Pode Monitor Service with an assembly version of 1.0.0. + +.EXAMPLE + Invoke-PodeBuildDotnetMonitorSrvBuild -DisableLifecycleServiceOperations + + Builds the Pode Monitor Service without lifecycle service operations. + +.EXAMPLE + Invoke-PodeBuildDotnetMonitorSrvBuild + + Builds the Pode Monitor Service for all target runtimes without a specific assembly version. +#> +function Invoke-PodeBuildDotnetMonitorSrvBuild() { + # Retrieve the highest installed SDK version + $majorVersion = ([version](dotnet --version)).Major + + # Determine if the target framework is compatible + $isCompatible = $majorVersions -ge $requiredSdkVersion + + # Skip build if not compatible + if ($isCompatible) { + Write-Output "SDK for target framework '$target' is compatible with the '$AvailableSdkVersion' framework." + } + else { + Write-Warning "SDK for target framework '$target' is not compatible with the '$AvailableSdkVersion' framework. Skipping build." + return + } + + # Optionally set assembly version + if ($Version) { + Write-Host "Assembly Version $Version" + $AssemblyVersion = "-p:Version=$Version" + } + else { + $AssemblyVersion = '' + } + + foreach ($target in @('win-x64', 'win-arm64' , 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64','linux-arm','win-x86','linux-musl-x64')) { + $DefineConstants = @() + $ParamConstants = '' + + # Add compilation constants if lifecycle operations are enabled + if (!$DisableLifecycleServiceOperations) { + $DefineConstants += 'ENABLE_LIFECYCLE_OPERATIONS' + } + + # Prepare constants for the build parameters + if ($DefineConstants.Count -gt 0) { + $ParamConstants = "-p:DefineConstants=`"$( $DefineConstants -join ';')`"" + } + + # Perform the build for the target runtime + dotnet publish --runtime $target --output ../Bin/$target --configuration Release $AssemblyVersion $ParamConstants + + # Throw an error if the build fails + if (!$?) { + throw "dotnet publish failed for $($target)" + } + } +} + <# .SYNOPSIS Retrieves the end-of-life (EOL) and supported versions of PowerShell. @@ -567,7 +664,7 @@ function Get-PodeBuildPwshEOL { .DESCRIPTION This function detects whether the current operating system is Windows by checking - the `$IsWindows` automatic variable, the presence of the `$env:ProgramFiles` variable, + the `Test-PodeBuildIsWindows` automatic variable, the presence of the `$env:ProgramFiles` variable, and the PowerShell Edition in `$PSVersionTable`. This function returns `$true` if any of these indicate Windows. @@ -896,8 +993,6 @@ function Split-PodeBuildPwshPath { } } - - # Check if the script is running under Invoke-Build if (($null -eq $PSCmdlet.MyInvocation) -or ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('BuildRoot') -and ($null -eq $BuildRoot))) { Write-Host 'This script is intended to be run with Invoke-Build. Please use Invoke-Build to execute the tasks defined in this script.' -ForegroundColor Yellow @@ -1098,9 +1193,6 @@ Add-BuildTask Build BuildDeps, { Remove-Item -Path ./src/Libs -Recurse -Force | Out-Null } - - - # Retrieve the SDK version being used # $dotnetVersion = dotnet --version @@ -1121,6 +1213,20 @@ Add-BuildTask Build BuildDeps, { Pop-Location } + if (Test-Path ./src/Bin) { + Remove-Item -Path ./src/Bin -Recurse -Force | Out-Null + } + + try { + Push-Location ./src/PodeMonitor + Invoke-PodeBuildDotnetMonitorSrvBuild + } + finally { + Pop-Location + } + + + } @@ -1202,7 +1308,7 @@ Add-BuildTask PackageFolder Build, { New-Item -Path $path -ItemType Directory -Force | Out-Null # which source folders do we need? create them and copy their contents - $folders = @('Private', 'Public', 'Misc', 'Libs', 'Locales') + $folders = @('Private', 'Public', 'Misc', 'Libs', 'Locales', 'Bin') $folders | ForEach-Object { New-Item -ItemType Directory -Path (Join-Path $path $_) -Force | Out-Null Copy-Item -Path "./src/$($_)/*" -Destination (Join-Path $path $_) -Force -Recurse | Out-Null @@ -1234,11 +1340,62 @@ Add-BuildTask TestNoBuild TestDeps, { Remove-Module Pester -Force -ErrorAction Ignore Import-Module Pester -Force -RequiredVersion $Versions.Pester } - + Write-Output '' # for windows, output current netsh excluded ports if (Test-PodeBuildIsWindows) { netsh int ipv4 show excludedportrange protocol=tcp | Out-Default + + # Retrieve the current Windows identity and token + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + + # Gather user information + $user = $identity.Name + $isElevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + $adminStatus = if ($isElevated) { 'Administrator' } else { 'Standard User' } + $groups = $identity.Groups | ForEach-Object { + try { + $_.Translate([Security.Principal.NTAccount]).Value + } + catch { + $_.Value # Fallback to SID if translation fails + } + } + + # Generate output + Write-Output 'Pester Execution Context (Windows):' + Write-Output " - User: $user" + Write-Output " - Role: $adminStatus" + Write-Output " - Elevated Privileges: $isElevated" + Write-Output " - Group Memberships: $( $groups -join ', ')" + } + + + if ($IsLinux) { + $user = whoami + $groupsRaw = (groups $user | Out-String).Trim() + $groups = $groupsRaw -split '\s+' | Where-Object { $_ -ne ':' } | Sort-Object -Unique + + # Check for sudo privileges based on group membership + $isSudoUser = $groups -match '\bwheel\b' -or $groups -match '\badmin\b' -or $groups -match '\bsudo\b' -or $groups -match '\badm\b' + + Write-Output 'Pester Execution Context (Linux):' + Write-Output " - User: $user" + Write-Output " - Groups: $( $groups -join ', ')" + Write-Output " - Sudo: $($isSudoUser -eq $true)" } + + if ($IsMacOS) { + $user = whoami + $groups = (id -Gn $user).Split(' ') # Use `id -Gn` for consistent group names on macOS + $formattedGroups = $groups -join ', ' + Write-Output 'Pester Execution Context (macOS):' + Write-Output " - User: $user" + Write-Output " - Groups: $formattedGroups" + } + + Write-Output '' + if ($UICulture -ne ([System.Threading.Thread]::CurrentThread.CurrentUICulture) ) { $originalUICulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture Write-Output "Original UICulture is $originalUICulture" @@ -1404,6 +1561,12 @@ Add-BuildTask CleanLibs { Remove-Item -Path $path -Recurse -Force | Out-Null } + $path = './src/Bin' + if (Test-Path -Path $path -PathType Container) { + Write-Host "Removing $path contents" + Remove-Item -Path $path -Recurse -Force | Out-Null + } + Write-Host "Cleanup $path done" } @@ -1610,7 +1773,7 @@ Add-BuildTask SetupPowerShell { #> # Synopsis: Build the Release Notes -task ReleaseNotes { +Add-BuildTask ReleaseNotes { if ([string]::IsNullOrWhiteSpace($ReleaseNoteVersion)) { Write-Host 'Please provide a ReleaseNoteVersion' -ForegroundColor Red return diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index d9617f469..1ee11608d 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -292,6 +292,14 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' localEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API." + serviceAlreadyRegisteredException = "الخدمة '{0}' مسجلة بالفعل." + serviceIsNotRegisteredException = "الخدمة '{0}' غير مسجلة." + serviceCommandFailedException = "فشل الأمر '{0}' في الخدمة '{1}'." + serviceRegistrationException = "فشل تسجيل الخدمة '{0}'." + serviceIsRunningException = "الخدمة '{0}' تعمل. استخدم المعامل -Force للإيقاف بالقوة." + serviceUnRegistrationException = "فشل إلغاء تسجيل الخدمة '{0}'." + passwordRequiredForServiceUserException = "مطلوب كلمة مرور عند تحديد مستخدم الخدمة في نظام Windows. يرجى تقديم كلمة مرور صالحة للمستخدم '{0}'." + featureNotSupportedException = '{0} مدعومة فقط على نظام التشغيل Windows.' suspendingMessage = 'تعليق' resumingMessage = 'استئناف' serverControlCommandsTitle = 'أوامر التحكم بالخادم:' diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index d85929f70..f74d1a653 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "Der Dienst '{0}' ist bereits registriert." + serviceIsNotRegisteredException = "Der Dienst '{0}' ist nicht registriert." + serviceCommandFailedException = "Der Dienstbefehl '{0}' ist bei dem Dienst '{1}' fehlgeschlagen." + serviceRegistrationException = "Die Registrierung des Dienstes '{0}' ist fehlgeschlagen." + serviceIsRunningException = "Der Dienst '{0}' läuft. Verwenden Sie den Parameter -Force, um den Dienst zwangsweise zu stoppen." + serviceUnRegistrationException = "Die Abmeldung des Dienstes '{0}' ist fehlgeschlagen." + passwordRequiredForServiceUserException = "Ein Passwort ist erforderlich, wenn ein Dienstbenutzer unter Windows angegeben wird. Bitte geben Sie ein gültiges Passwort für den Benutzer '{0}' an." + featureNotSupportedException = '{0} wird nur unter Windows unterstützt.' suspendingMessage = 'Anhalten' resumingMessage = 'Fortsetzen' serverControlCommandsTitle = 'Serversteuerbefehle:' diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index c2ac6d2b0..ebaa0c836 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -291,7 +291,15 @@ 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." + serviceAlreadyRegisteredException = "Service '{0}' is already registered." + serviceIsNotRegisteredException = "Service '{0}' is not registered." + serviceCommandFailedException = "Service command '{0}' failed on service '{1}'." + serviceRegistrationException = "Service '{0}' registration failed." + serviceIsRunningException = "Service '{0}' is running. Use the -Force parameter to forcefully stop." + serviceUnRegistrationException = "Service '{0}' unregistration failed." + passwordRequiredForServiceUserException = "A password is required when specifying a service user on Windows. Please provide a valid password for the user '{0}'." + featureNotSupportedException = '{0} is supported only on Windows.' suspendingMessage = 'Suspending' resumingMessage = 'Resuming' serverControlCommandsTitle = 'Server Control Commands:' diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index fc04db0d2..f92fd4292 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "Service '{0}' is already registered." + serviceIsNotRegisteredException = "Service '{0}' is not registered." + serviceCommandFailedException = "Service command '{0}' failed on service '{1}'." + serviceRegistrationException = "Service '{0}' registration failed." + serviceIsRunningException = "Service '{0}' is running. Use the -Force parameter to forcefully stop." + serviceUnRegistrationException = "Service '{0}' unregistration failed." + passwordRequiredForServiceUserException = "A password is required when specifying a service user on Windows. Please provide a valid password for the user '{0}'." + featureNotSupportedException = '{0} is supported only on Windows.' suspendingMessage = 'Suspending' resumingMessage = 'Resuming' serverControlCommandsTitle = 'Server Control Commands:' diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index d7db64e6c..8818d8a1f 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "El servicio '{0}' ya está registrado." + serviceIsNotRegisteredException = "El servicio '{0}' no está registrado." + serviceCommandFailedException = "El comando del servicio '{0}' falló en el servicio '{1}'." + serviceRegistrationException = "Falló el registro del servicio '{0}'." + serviceIsRunningException = "El servicio '{0}' está en ejecución. Utilice el parámetro -Force para detenerlo a la fuerza." + serviceUnRegistrationException = "La anulación del registro del servicio '{0}' falló." + passwordRequiredForServiceUserException = "Se requiere una contraseña al especificar un usuario de servicio en Windows. Por favor, proporcione una contraseña válida para el usuario '{0}'." + featureNotSupportedException = '{0} solo es compatible con Windows.' suspendingMessage = 'Suspendiendo' resumingMessage = 'Reanudando' serverControlCommandsTitle = 'Comandos de control del servidor:' diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index a0d769eab..a217413a1 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "Le service '{0}' est déjà enregistré." + serviceIsNotRegisteredException = "Le service '{0}' n'est pas enregistré." + serviceCommandFailedException = "La commande de service '{0}' a échoué sur le service '{1}'." + serviceRegistrationException = "Échec de l'enregistrement du service '{0}'." + serviceIsRunningException = "Le service '{0}' est en cours d'exécution. Utilisez le paramètre -Force pour forcer l'arrêt." + serviceUnRegistrationException = "La désinscription du service '{0}' a échoué." + passwordRequiredForServiceUserException = "Un mot de passe est requis lors de la spécification d'un utilisateur de service sous Windows. Veuillez fournir un mot de passe valide pour l'utilisateur '{0}'." + featureNotSupportedException = '{0} est pris en charge uniquement sous Windows.' suspendingMessage = 'Suspension' resumingMessage = 'Reprise' serverControlCommandsTitle = 'Commandes de contrôle du serveur :' diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 5dd3c47f4..9049238d2 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "Il servizio '{0}' è già registrato." + serviceIsNotRegisteredException = "Il servizio '{0}' non è registrato." + serviceCommandFailedException = "Il comando '{0}' è fallito sul servizio '{1}'." + serviceRegistrationException = "Registrazione del servizio '{0}' non riuscita." + serviceIsRunningException = "Il servizio '{0}' è in esecuzione. Utilizzare il parametro -Force per interromperlo forzatamente." + serviceUnRegistrationException = "La cancellazione della registrazione del servizio '{0}' è fallita." + passwordRequiredForServiceUserException = "È richiesta una password quando si specifica un utente del servizio su Windows. Si prega di fornire una password valida per l'utente '{0}'." + featureNotSupportedException = '{0} è supportato solo su Windows.' suspendingMessage = 'Sospensione' resumingMessage = 'Ripresa' serverControlCommandsTitle = 'Comandi di controllo del server:' diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 43d8243bc..2d54d26f6 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -292,6 +292,14 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。" unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' localEndpointConflictExceptionMessage = "'{0}' と '{1}' は OpenAPI のローカルエンドポイントとして定義されていますが、API 定義ごとに 1 つのローカルエンドポイントのみ許可されます。" + serviceAlreadyRegisteredException = "サービス '{0}' はすでに登録されています。" + serviceIsNotRegisteredException = "サービス '{0}' は登録されていません。" + serviceCommandFailedException = "サービスコマンド '{0}' はサービス '{1}' で失敗しました。" + serviceRegistrationException = "サービス '{0}' の登録に失敗しました。" + serviceIsRunningException = "サービス '{0}' が実行中です。強制的に停止するには、-Force パラメーターを使用してください。" + serviceUnRegistrationException = "サービス '{0}' の登録解除に失敗しました。" + passwordRequiredForServiceUserException = "Windowsでサービスユーザーを指定する際にはパスワードが必要です。ユーザー '{0}' に有効なパスワードを入力してください。" + featureNotSupportedException = '{0} は Windows のみでサポートされています。' suspendingMessage = '停止' resumingMessage = '再開' serverControlCommandsTitle = 'サーバーコントロールコマンド:' diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index a0d882588..9d55a61a3 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -292,6 +292,14 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' localEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." + serviceAlreadyRegisteredException = "서비스 '{0}'가 이미 등록되었습니다." + serviceIsNotRegisteredException = "서비스 '{0}'가 등록되지 않았습니다." + serviceCommandFailedException = "서비스 명령 '{0}' 이(가) 서비스 '{1}' 에서 실패했습니다." + serviceRegistrationException = "서비스 '{0}' 등록에 실패했습니다." + serviceIsRunningException = "서비스 '{0}'가 실행 중입니다. 강제로 중지하려면 -Force 매개변수를 사용하세요." + serviceUnRegistrationException = "서비스 '{0}' 등록 취소에 실패했습니다." + passwordRequiredForServiceUserException = "Windows에서 서비스 사용자를 지정할 때는 비밀번호가 필요합니다. 사용자 '{0}'에 대해 유효한 비밀번호를 입력하세요." + featureNotSupportedException = '{0}는 Windows에서만 지원됩니다.' suspendingMessage = '중단' resumingMessage = '재개' serverControlCommandsTitle = '서버 제어 명령:' diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 6aad6ebc4..d297fa58e 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "De service '{0}' is al geregistreerd." + serviceIsNotRegisteredException = "De service '{0}' is niet geregistreerd." + serviceCommandFailedException = "De serviceopdracht '{0}' is mislukt op de service '{1}'." + serviceRegistrationException = "Registratie van de service '{0}' is mislukt." + serviceIsRunningException = "De service '{0}' draait. Gebruik de parameter -Force om de service geforceerd te stoppen." + serviceUnRegistrationException = "Het afmelden van de service '{0}' is mislukt." + passwordRequiredForServiceUserException = "Een wachtwoord is vereist bij het specificeren van een servicegebruiker in Windows. Geef een geldig wachtwoord op voor de gebruiker '{0}'." + featureNotSupportedException = '{0} wordt alleen ondersteund op Windows.' suspendingMessage = 'Onderbreken' resumingMessage = 'Hervatten' serverControlCommandsTitle = "Serverbedieningscommando's:" diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index cf5dd507b..268ad1dbf 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "Usługa '{0}' jest już zarejestrowana." + serviceIsNotRegisteredException = "Usługa '{0}' nie jest zarejestrowana." + serviceCommandFailedException = "Polecenie serwisu '{0}' nie powiodło się w serwisie '{1}'." + serviceRegistrationException = "Rejestracja usługi '{0}' nie powiodła się." + serviceIsRunningException = "Usługa '{0}' jest uruchomiona. Użyj parametru -Force, aby wymusić zatrzymanie." + serviceUnRegistrationException = "Nie udało się wyrejestrować usługi '{0}'." + passwordRequiredForServiceUserException = "Wymagane jest hasło podczas określania użytkownika usługi w systemie Windows. Podaj prawidłowe hasło dla użytkownika '{0}'." + featureNotSupportedException = '{0} jest obsługiwane tylko w systemie Windows.' suspendingMessage = 'Wstrzymywanie' resumingMessage = 'Wznawianie' serverControlCommandsTitle = 'Polecenia sterowania serwerem:' diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index b8e1e4cd1..cc3041e32 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -292,6 +292,14 @@ 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." + serviceAlreadyRegisteredException = "O serviço '{0}' já está registrado." + serviceIsNotRegisteredException = "O serviço '{0}' não está registrado." + serviceCommandFailedException = "O comando do serviço '{0}' falhou no serviço '{1}'." + serviceRegistrationException = "Falha no registro do serviço '{0}'." + serviceIsRunningException = "O serviço '{0}' está em execução. Use o parâmetro -Force para forçar a parada." + serviceUnRegistrationException = "A anulação do registro do serviço '{0}' falhou." + passwordRequiredForServiceUserException = "Uma senha é necessária ao especificar um usuário de serviço no Windows. Por favor, forneça uma senha válida para o usuário '{0}'." + featureNotSupportedException = '{0} é compatível apenas com o Windows.' suspendingMessage = 'Suspensão' resumingMessage = 'Retomada' serverControlCommandsTitle = 'Comandos de controle do servidor:' diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 3653dc151..d98d66f5b 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -292,6 +292,14 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' localEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" + serviceAlreadyRegisteredException = "服务 '{0}' 已经注册。" + serviceIsNotRegisteredException = "服务 '{0}' 未注册。" + serviceCommandFailedException = "服务命令 '{0}' 在服务 '{1}' 上失败。" + serviceRegistrationException = "服务 '{0}' 注册失败。" + serviceIsRunningException = "服务 '{0}' 正在运行。使用 -Force 参数强制停止。" + serviceUnRegistrationException = "服务 '{0}' 的注销失败。" + passwordRequiredForServiceUserException = "在 Windows 中指定服务用户时需要密码。请为用户 '{0}' 提供有效的密码。" + featureNotSupportedException = '{0} 仅支持 Windows。' suspendingMessage = '暂停' resumingMessage = '恢复' serverControlCommandsTitle = '服务器控制命令:' diff --git a/src/Pode.psd1 b/src/Pode.psd1 index fa158a2ec..75a95c1bc 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -528,6 +528,17 @@ 'New-PodeLimitEndpointComponent', 'New-PodeLimitMethodComponent', 'New-PodeLimitHeaderComponent' + 'Use-PodeScopedVariables', + + # service + 'Register-PodeService', + 'Unregister-PodeService', + 'Start-PodeService', + 'Stop-PodeService', + 'Get-PodeService', + 'Suspend-PodeService', + 'Resume-PodeService', + 'Restart-PodeService' ) # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. diff --git a/src/Pode.psm1 b/src/Pode.psm1 index 8e117f136..213c86c7d 100644 --- a/src/Pode.psm1 +++ b/src/Pode.psm1 @@ -146,6 +146,15 @@ try { Export-ModuleMember -Function ($funcs.Name) } } + + # Define Properties Display + if (!(Get-TypeData -TypeName 'PodeService')) { + $TypeData = @{ + TypeName = 'PodeService' + DefaultDisplayPropertySet = 'Name', 'Status', 'Pid' + } + Update-TypeData @TypeData + } } catch { throw ("Failed to load the Pode module. $_") diff --git a/src/PodeMonitor/IPausableHostedService.cs b/src/PodeMonitor/IPausableHostedService.cs new file mode 100644 index 000000000..ce0426fbe --- /dev/null +++ b/src/PodeMonitor/IPausableHostedService.cs @@ -0,0 +1,26 @@ +namespace PodeMonitor +{ + /// + /// Defines a contract for a hosted service that supports pausing and resuming. + /// + public interface IPausableHostedService + { + /// + /// Pauses the hosted service. + /// This method is called when the service receives a pause command. + /// + void OnPause(); + + /// + /// Resumes the hosted service. + /// This method is called when the service receives a continue command after being paused. + /// + void OnContinue(); + + + + void Restart(); + + public PodeMonitorServiceState State { get; } + } +} diff --git a/src/PodeMonitor/PipeNameGenerator.cs b/src/PodeMonitor/PipeNameGenerator.cs new file mode 100644 index 000000000..6a4299dba --- /dev/null +++ b/src/PodeMonitor/PipeNameGenerator.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +namespace PodeMonitor +{ + public static class PipeNameGenerator + { + private const int MaxUnixPathLength = 104; // Max length for Unix domain sockets on macOS + private const string UnixTempDir = "/tmp"; // Short temporary directory for Unix systems + + public static string GeneratePipeName() + { + // Generate a unique name based on a GUID + string uniqueId = Guid.NewGuid().ToString("N").Substring(0, 8); + + if (OperatingSystem.IsWindows()) + { + // Use Windows named pipe format + return $"PodePipe_{uniqueId}"; + } + else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + // Use Unix domain socket format with a shorter temp directory + //string pipePath = Path.Combine(UnixTempDir, $"PodePipe_{uniqueId}"); + string pipePath = $"PodePipe_{uniqueId}"; + + // Ensure the path is within the allowed length for Unix domain sockets + if (pipePath.Length > MaxUnixPathLength) + { + throw new InvalidOperationException($"Generated pipe path exceeds the maximum length of {MaxUnixPathLength} characters: {pipePath}"); + } + + return pipePath; + } + else + { + throw new PlatformNotSupportedException("Unsupported operating system for pipe name generation."); + } + } + } +} diff --git a/src/PodeMonitor/PodeLogLevel.cs b/src/PodeMonitor/PodeLogLevel.cs new file mode 100644 index 000000000..ed91a0884 --- /dev/null +++ b/src/PodeMonitor/PodeLogLevel.cs @@ -0,0 +1,15 @@ + +namespace PodeMonitor +{ + /// + /// Enum representing the various log levels for PodeMonitorLogger. + /// + public enum PodeLogLevel + { + DEBUG, // Detailed information for debugging purposes + INFO, // General operational information + WARN, // Warning messages for potential issues + ERROR, // Error messages for failures + CRITICAL // Critical errors indicating severe failures + } +} \ No newline at end of file diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs new file mode 100644 index 000000000..446c050cb --- /dev/null +++ b/src/PodeMonitor/PodeMonitor.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Threading; + +namespace PodeMonitor +{ + + /// + /// Class responsible for managing and monitoring the Pode PowerShell process. + /// Provides functionality for starting, stopping, suspending, resuming, and restarting the process. + /// Communicates with the Pode process via named pipes. + /// + public class PodeMonitor + { + private readonly object _syncLock = new(); // Synchronization lock for thread safety + private Process _powerShellProcess; // PowerShell process instance + private NamedPipeClientStream _pipeClient; // Named pipe client for inter-process communication + + // Configuration properties + private readonly string _scriptPath; // Path to the Pode script + private readonly string _parameterString; // Parameters passed to the script + private readonly string _pwshPath; // Path to the PowerShell executable + private readonly int _shutdownWaitTimeMs; // Timeout for shutting down the process + private readonly string _stateFilePath; // Path to the service state file + + private DateTime _lastLogTime; // Tracks the last time the process logged activity + + public int StartMaxRetryCount { get; } // Maximum number of retries for starting the process + public int StartRetryDelayMs { get; } // Delay between retries in milliseconds + + private volatile PodeMonitorServiceState _state; + + + public PodeMonitorServiceState State { get => _state; set => _state = value; } + + public bool DisableTermination { get => _serviceJson.DisableTermination; } + + private class ServiceJson(PodeMonitorWorkerOptions options) + { + public readonly string PipeName = PipeNameGenerator.GeneratePipeName(); // Name of the named pipe for communication + public readonly bool Quiet = options.Quiet; // Indicates whether the process runs in quiet mode + public readonly bool DisableTermination = options.DisableTermination; // Indicates whether termination is disabled + public readonly bool DisableConsoleInput = options.DisableConsoleInput; // Disables all console interactions for the server + public readonly bool IgnoreServerConfig = options.IgnoreServerConfig; // Prevents the server from loading settings from the server.psd1 configuration file + public readonly string ConfigFile = options.ConfigFile.Replace("\\", "\\\\"); // Specifies a custom configuration file instead of using the default `server.psd1` + } + + private readonly ServiceJson _serviceJson; + + /// + /// Initializes a new instance of the class with the specified configuration options. + /// + /// Configuration options for the PodeMonitor. + public PodeMonitor(PodeMonitorWorkerOptions options) + { + // Initialize configuration properties + _scriptPath = options.ScriptPath; + _pwshPath = options.PwshPath; + _parameterString = options.ParameterString; + _shutdownWaitTimeMs = options.ShutdownWaitTimeMs; + StartMaxRetryCount = options.StartMaxRetryCount; + StartRetryDelayMs = options.StartRetryDelayMs; + + // Initialize the _serviceJson object with values from options + _serviceJson = new ServiceJson(options); + + // Define the state file path only for Linux/macOS + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + + string homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + string stateDirectory = OperatingSystem.IsLinux() + ? "/run/podemonitor" + : OperatingSystem.IsMacOS() + ? Path.Combine(homeDirectory, "Library", "LaunchAgents", "PodeMonitor") + : throw new PlatformNotSupportedException("The current platform is not supported for setting the state directory."); + try + { + if (!Directory.Exists(stateDirectory)) + { + Directory.CreateDirectory(stateDirectory); + } + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, + $"Failed to create state directory at {stateDirectory}: {ex.Message}"); + throw; + } + + // Define the state file path (default to /var/tmp for Linux/macOS) + _stateFilePath = Path.Combine(stateDirectory, $"{Environment.ProcessId}.state"); + + + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initialized PodeMonitor with pipe name: {0} and state file: {1}", _serviceJson.PipeName, _stateFilePath); + } + } + + /// + /// Starts the Pode PowerShell process. If the process is already running, logs its status. + /// + public void StartPowerShellProcess() + { + lock (_syncLock) + { + if (_powerShellProcess != null && !_powerShellProcess.HasExited) + { + if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is Alive."); + _lastLogTime = DateTime.Now; + } + return; + } + + try + { + // Configure the PowerShell process + _powerShellProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = _pwshPath, + Arguments = BuildCommand(), + RedirectStandardOutput = true, // Redirect standard output for logging + RedirectStandardError = true, // Redirect standard error for logging + UseShellExecute = false, // Run without using shell execution + CreateNoWindow = true // Prevent the creation of a window + } + }; + + // Subscribe to the output stream for logging and state parsing + _powerShellProcess.OutputDataReceived += (sender, args) => + { + if (!string.IsNullOrEmpty(args.Data)) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); + ParseServiceState(args.Data); + } + }; + + // Subscribe to the error stream for logging errors + _powerShellProcess.ErrorDataReceived += (sender, args) => + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); + }; + + // Start the process and begin reading the output/error streams + _powerShellProcess.Start(); + _powerShellProcess.BeginOutputReadLine(); + _powerShellProcess.BeginErrorReadLine(); + + _lastLogTime = DateTime.Now; + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process started successfully."); + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to start Pode process: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); + } + } + } + + /// + /// Stops the Pode PowerShell process gracefully. If it does not terminate, it is forcefully killed. + /// + public void StopPowerShellProcess() + { + lock (_syncLock) + { + if (_powerShellProcess == null || (_powerShellProcess.HasExited && Process.GetProcessById(_powerShellProcess.Id) == null)) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is not running."); + return; + } + + try + { + if (InitializePipeClientWithRetry()) + { + SendPipeMessage("stop"); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); + WaitForProcessExit(_shutdownWaitTimeMs); + + if (_powerShellProcess != null && !_powerShellProcess.HasExited) + { + PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Pode process has exited:{_powerShellProcess.HasExited} Id:{_powerShellProcess.Id}"); + + PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully. Killing process."); + _powerShellProcess.Kill(); + } + + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process stopped successfully."); + } + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error stopping Pode process: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); + } + finally + { + CleanupResources(); + } + } + } + + /// + /// Sends a suspend command to the Pode process via named pipe. + /// + public void SuspendPowerShellProcess() => ExecutePipeCommand("suspend"); + + /// + /// Sends a resume command to the Pode process via named pipe. + /// + public void ResumePowerShellProcess() => ExecutePipeCommand("resume"); + + /// + /// Sends a restart command to the Pode process via named pipe. + /// + public void RestartPowerShellProcess() => ExecutePipeCommand("restart"); + + + + /// + /// Executes a command by sending it to the Pode process via named pipe. + /// + /// The command to execute (e.g., "suspend", "resume", "restart"). + private void ExecutePipeCommand(string command) + { + lock (_syncLock) + { + try + { + if (InitializePipeClientWithRetry()) + { + SendPipeMessage(command); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"{command.ToUpper()} command sent to Pode process."); + } + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error executing {command} command: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); + } + finally + { + CleanupPipeClient(); + } + } + } + + /// + /// Parses the service state from the provided output message and updates the state variables. + /// + /// The output message containing the service state. + private void ParseServiceState(string output) + { + if (string.IsNullOrWhiteSpace(output)) return; + + if (output.StartsWith("Service State: ", StringComparison.OrdinalIgnoreCase)) + { + string state = output["Service State: ".Length..].Trim(); + + // Convert the extracted string to a PodeMonitorServiceState enum + PodeMonitorServiceState parsedState = state.ToPodeMonitorServiceState(); + + // Update the service state + UpdateServiceState(parsedState); + } + } + + /// + /// Updates the internal state variables based on the provided service state. + /// + /// The new service state. + private void UpdateServiceState(PodeMonitorServiceState state) + { + _state = state; + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state updated to: {state}"); + // Write the state to the state file only on Linux/macOS + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + // Write the state to the state file + WriteServiceStateToFile(state); + } + } + + /// + /// Builds the PowerShell command to execute the Pode process. + /// + /// The PowerShell command string. + private string BuildCommand() + { + string podeServiceJson = + $"{{\\\"DisableTermination\\\": {_serviceJson.DisableTermination.ToString().ToLower()}, " + + $"\\\"Quiet\\\": {_serviceJson.Quiet.ToString().ToLower()}, " + + $"\\\"DisableConsoleInput\\\": {_serviceJson.DisableConsoleInput.ToString().ToLower()}, " + + $"\\\"IgnoreServerConfig\\\": {_serviceJson.IgnoreServerConfig.ToString().ToLower()}, " + + $"\\\"ConfigFile\\\": \\\"{_serviceJson.ConfigFile}\\\", " + + $"\\\"PipeName\\\": \\\"{_serviceJson.PipeName}\\\"}}"; + + return $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; + } + + + /// + /// Initializes the named pipe client with a retry mechanism. + /// + /// The maximum number of retries for connection. + /// True if the pipe client is successfully connected; otherwise, false. + private bool InitializePipeClientWithRetry(int maxRetries = 3) + { + int attempts = 0; + + while (attempts < maxRetries) + { + try + { + if (_pipeClient == null) + { + _pipeClient = new NamedPipeClientStream(".", _serviceJson.PipeName, PipeDirection.InOut); + } + + if (!_pipeClient.IsConnected) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Connecting to pipe server (Attempt {attempts + 1})..."); + _pipeClient.Connect(10000); // Timeout of 10 seconds + } + + return _pipeClient.IsConnected; + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Pipe connection attempt {attempts + 1} failed: {ex.Message}"); + } + + attempts++; + Thread.Sleep(1000); + } + + return false; + } + + /// + /// Sends a message to the Pode process via named pipe. + /// + /// The message to send. + private void SendPipeMessage(string message) + { + try + { + using var writer = new StreamWriter(_pipeClient) { AutoFlush = true }; + writer.WriteLine(message); + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error sending message to pipe: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); + } + } + + /// + /// Waits for the Pode process to exit within the specified timeout. + /// + /// The timeout period in milliseconds. + private void WaitForProcessExit(int timeout) + { + int waited = 0; + while (!_powerShellProcess.HasExited && waited < timeout) + { + Thread.Sleep(200); + waited += 200; + } + } + + + /// + /// Writes the current service state to the state file. + /// + /// The service state to write. + private void WriteServiceStateToFile(PodeMonitorServiceState state) + { + lock (_syncLock) // Ensure thread-safe access + { + try + { + File.WriteAllText(_stateFilePath, state.ToString().ToLowerInvariant()); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Service state written to file: {_stateFilePath}"); + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to write service state to file: {ex.Message}"); + } + } + } + + /// + /// Deletes the service state file during cleanup. + /// + private void DeleteServiceStateFile() + { + lock (_syncLock) // Ensure thread-safe access + { + try + { + if (File.Exists(_stateFilePath)) + { + File.Delete(_stateFilePath); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state file deleted: {_stateFilePath}"); + } + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to delete service state file: {ex.Message}"); + } + } + } + + /// + /// Cleans up resources associated with the Pode process and the pipe client. + /// + private void CleanupResources() + { + _powerShellProcess?.Dispose(); + _powerShellProcess = null; + + CleanupPipeClient(); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + DeleteServiceStateFile(); + } + } + + /// + /// Cleans up the named pipe client. + /// + private void CleanupPipeClient() + { + _pipeClient?.Dispose(); + _pipeClient = null; + } + } +} \ No newline at end of file diff --git a/src/PodeMonitor/PodeMonitor.csproj b/src/PodeMonitor/PodeMonitor.csproj new file mode 100644 index 000000000..cba19cd46 --- /dev/null +++ b/src/PodeMonitor/PodeMonitor.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + true + true + true + true + false + win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64;linux-arm;win-x86;linux-musl-x64 + + + + + + + + + + + + + + + diff --git a/src/PodeMonitor/PodeMonitorLogger.cs b/src/PodeMonitor/PodeMonitorLogger.cs new file mode 100644 index 000000000..35e841568 --- /dev/null +++ b/src/PodeMonitor/PodeMonitorLogger.cs @@ -0,0 +1,219 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; + +namespace PodeMonitor +{ + + /// + /// A thread-safe logger for PodeMonitor that supports log rotation, exception logging, and log level filtering. + /// + public static partial class PodeMonitorLogger + { + private static readonly object _logLock = new(); // Ensures thread-safe writes + private static string _logFilePath = "PodeService.log"; // Default log file path + private static PodeLogLevel _minLogLevel = PodeLogLevel.INFO; // Default minimum log level + private const long DefaultMaxFileSize = 10 * 1024 * 1024; // Default max file size: 10 MB + + [GeneratedRegex(@"\x1B\[[0-9;]*[a-zA-Z]")] + private static partial Regex AnsiRegex(); + + /// + /// Initializes the logger with a custom log file path and minimum log level. + /// Validates the path, ensures the log file exists, and sets up log rotation. + /// + /// Path to the log file. + /// Minimum log level to record. + /// Maximum log file size in bytes before rotation. + public static void Initialize(string filePath, PodeLogLevel level, long maxFileSizeInBytes = DefaultMaxFileSize) + { + try + { + // Set the log file path and validate it + if (!string.IsNullOrWhiteSpace(filePath)) + { + ValidateLogPath(filePath); + _logFilePath = filePath; + } + + _minLogLevel = level; + + // Ensure the log file exists + if (!File.Exists(_logFilePath)) + { + using (File.Create(_logFilePath)) { }; + } + + // Perform log rotation if necessary + RotateLogFile(maxFileSizeInBytes); + + // Log initialization success + Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, + "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}, MaxFileSize: {2} bytes", _logFilePath, _minLogLevel, maxFileSizeInBytes); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to initialize logger: {ex.Message}"); + } + } + + /// + /// Logs a message to the log file with the specified log level, context, and optional arguments. + /// + /// Log level. + /// Context of the log (e.g., "PodeMonitor"). + /// Process ID to include in the log. + /// Message to log. + /// Optional arguments for formatting the message. + public static void Log(PodeLogLevel level, string context, int pid, string message = "", params object[] args) + { + if (level < _minLogLevel || string.IsNullOrEmpty(message)) + { + return; // Skip logging for levels below the minimum or empty messages + } + + try + { + // Sanitize the message to remove ANSI escape codes + string sanitizedMessage = AnsiRegex().Replace(message, string.Empty); + + // Format the sanitized message + string formattedMessage = string.Format(sanitizedMessage, args); + + // Get the current time in ISO 8601 format (UTC) + string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + + // Construct the log entry + string logEntry = $"{timestamp} [PID:{pid}] [{level}] [{context}] {formattedMessage}"; + + // Thread-safe log file write + lock (_logLock) + { + using StreamWriter writer = new(_logFilePath, true); + writer.WriteLine(logEntry); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to log to file:"); + Console.WriteLine($"{context} - {message}"); + Console.WriteLine($"Error: {ex.Message}"); + } + } + + /// + /// Logs an exception and an optional message to the log file. + /// Includes exception stack trace and inner exception details. + /// + /// Log level. + /// Exception to log. + /// Optional message to include. + /// Optional arguments for formatting the message. + public static void Log(PodeLogLevel level, Exception exception, string message = null, params object[] args) + { + if (level < _minLogLevel || (exception == null && string.IsNullOrEmpty(message))) + { + return; // Skip logging if the level is below the minimum or there's nothing to log + } + + try + { + // Get the current time in ISO 8601 format (UTC) + string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + + // Format the message if provided + string logMessage = string.Empty; + + if (!string.IsNullOrEmpty(message)) + { + // Sanitize the message to remove ANSI escape codes + string sanitizedMessage = AnsiRegex().Replace(message, string.Empty); + logMessage = string.Format(sanitizedMessage, args); + } + + // Add exception details + if (exception != null) + { + logMessage += $"{Environment.NewLine}Exception: {exception.GetType().Name}"; + logMessage += $"{Environment.NewLine}Message: {exception.Message}"; + logMessage += $"{Environment.NewLine}Stack Trace: {exception.StackTrace}"; + + // Include inner exception details if any + var innerException = exception.InnerException; + while (innerException != null) + { + logMessage += $"{Environment.NewLine}Inner Exception: {innerException.GetType().Name}"; + logMessage += $"{Environment.NewLine}Message: {innerException.Message}"; + logMessage += $"{Environment.NewLine}Stack Trace: {innerException.StackTrace}"; + innerException = innerException.InnerException; + } + } + + // Construct the log entry + string logEntry = $"{timestamp} [PID:{Environment.ProcessId}] [{level}] [PodeMonitor] {logMessage}"; + + // Thread-safe log file write + lock (_logLock) + { + using StreamWriter writer = new(_logFilePath, true); + writer.WriteLine(logEntry); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to log exception to file: {ex.Message}"); + } + } + + /// + /// Ensures log rotation by renaming old logs when the current log file exceeds the specified size. + /// + /// Maximum size of the log file in bytes before rotation. + private static void RotateLogFile(long maxFileSizeInBytes) + { + lock (_logLock) + { + FileInfo logFile = new(_logFilePath); + if (logFile.Exists && logFile.Length > maxFileSizeInBytes) + { + string rotatedFilePath = $"{_logFilePath}.{DateTime.UtcNow:yyyyMMddHHmmss}"; + File.Move(_logFilePath, rotatedFilePath); + } + } + } + + /// + /// Validates the log file path to ensure it is writable. + /// Creates the directory if it does not exist. + /// + /// Path to validate. + private static void ValidateLogPath(string filePath) + { + string directory = Path.GetDirectoryName(filePath); + + if (string.IsNullOrWhiteSpace(directory)) + { + throw new ArgumentException("Invalid log file path: Directory cannot be determined."); + } + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + try + { + string testFilePath = Path.Combine(directory, "test_write.log"); + using (var stream = File.Create(testFilePath)) + { + stream.WriteByte(0); + } + File.Delete(testFilePath); + } + catch (Exception ex) + { + throw new IOException($"Log directory is not writable: {directory}", ex); + } + } + } +} diff --git a/src/PodeMonitor/PodeMonitorMain.cs b/src/PodeMonitor/PodeMonitorMain.cs new file mode 100644 index 000000000..e0945d3fe --- /dev/null +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -0,0 +1,232 @@ +using System; +using System.IO; +using System.ServiceProcess; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using System.Text.Json; +using Microsoft.Extensions.Options; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Microsoft.Extensions.Logging; + +namespace PodeMonitor +{ + /// + /// Entry point for the Pode service. Handles platform-specific configurations and signal-based operations. + /// + public static partial class Program + { + // Platform-dependent signal registration (for linux/macOS) + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void SignalHandler(int signum); + + [LibraryImport("libc", EntryPoint = "signal")] + private static partial int Signal(int signum, SignalHandler handler); + + private const int SIGTSTP = 20; // Signal for pause + private const int SIGCONT = 18; // Signal for continue + private const int SIGHUP = 1; // Signal for restart + private const int SIGTERM = 15; // Signal for gracefully terminate a process. + + private static PodeMonitorWorker _workerInstance; // Global instance for managing worker operations + + /// + /// Entry point for the Pode service. + /// + /// Command-line arguments. + public static void Main(string[] args) + { + string customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Default config file + string serviceName = "PodeService"; + + // Check if the custom configuration file exists + if (!File.Exists(customConfigFile)) + { + Console.WriteLine($"Configuration file '{customConfigFile}' not found. Please provide a valid configuration file."); + Environment.Exit(1); // Exit with a non-zero code to indicate failure + } + + // Load configuration + IConfigurationRoot config = new ConfigurationBuilder() + .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) + .Build(); + + serviceName = config.GetSection("PodeMonitorWorker:Name").Value ?? serviceName; + + string logFilePath = config.GetSection("PodeMonitorWorker:logFilePath").Value ?? "PodeMonitorService.log"; + + // Parse log level + string logLevelString = config.GetSection("PodeMonitorWorker:LogLevel").Value; + + if (!Enum.TryParse(logLevelString, true, out PodeLogLevel logLevel)) + { + Console.WriteLine($"Invalid or missing log level '{logLevelString}'. Defaulting to INFO."); + logLevel = PodeLogLevel.INFO; // Default log level + } + else + { + Console.WriteLine($"Log level set to '{logLevelString}'."); + } + + // Parse log max file size + string logMaxFileSizeString = config.GetSection("PodeMonitorWorker:LogMaxFileSize").Value; + if (!long.TryParse(logMaxFileSizeString, out long logMaxFileSize)) + { + Console.WriteLine($"Invalid or missing log max file size '{logMaxFileSizeString}'. Defaulting to 10 MB."); + logMaxFileSize = 10 * 1024 * 1024; // Default to 10 MB + } + // Initialize logger + PodeMonitorLogger.Initialize(logFilePath, logLevel, logMaxFileSize); + + // Configure host builder + var builder = CreateHostBuilder(args, customConfigFile); + + // Platform-specific logic + if (OperatingSystem.IsLinux()) + { + ConfigureLinux(builder); + } + else if (OperatingSystem.IsWindows()) + { + ConfigureWindows(builder, serviceName); + } + else if (OperatingSystem.IsMacOS()) + { + ConfigureMacOS(builder); + } + else + { + PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Unsupported platform. Exiting."); + } + } + + /// + /// Creates and configures the host builder for the Pode service. + /// + /// Command-line arguments. + /// Path to the custom configuration file. + /// The configured host builder. + private static IHostBuilder CreateHostBuilder(string[] args, string customConfigFile) + { + return Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration(config => + { + config.AddJsonFile(customConfigFile, optional: false, reloadOnChange: true); + }) + .ConfigureServices((context, services) => + { + services.Configure(context.Configuration.GetSection("PodeMonitorWorker")); + + // Register PodeMonitor + services.AddSingleton(serviceProvider => + { + var options = serviceProvider.GetRequiredService>().Value; + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initializing PodeMonitor with options: {0}", options.ToString()); + return new PodeMonitor(options); + }); + + // Register PodeMonitorWorker and track the instance + services.AddSingleton(provider => + { + var logger = provider.GetRequiredService>(); + var monitor = provider.GetRequiredService(); + var worker = new PodeMonitorWorker(logger, monitor); + _workerInstance = worker; // Track the instance globally + return worker; + }); + + // Add PodeMonitorWorker as a hosted service + services.AddHostedService(provider => provider.GetRequiredService()); + + // Register IPausableHostedService + services.AddSingleton(provider => provider.GetRequiredService()); + }); + } + +#if ENABLE_LIFECYCLE_OPERATIONS + /// + /// Configures the Pode service for linux, including signal handling. + /// + /// The host builder. + [SupportedOSPlatform("linux")] + private static void ConfigureLinux(IHostBuilder builder) + { + // Handle linux signals for pause, resume, and restart + Signal(SIGTSTP, HandleSignalStop); + Signal(SIGCONT, HandleSignalContinue); + Signal(SIGHUP, HandleSignalRestart); + builder.UseSystemd(); + builder.Build().Run(); + } + + /// + /// Configures the Pode service for macOS, including signal handling. + /// + /// The host builder. + [SupportedOSPlatform("macOS")] + private static void ConfigureMacOS(IHostBuilder builder) + { + // Use launchd for macOS + Signal(SIGTSTP, HandleSignalStop); + Signal(SIGCONT, HandleSignalContinue); + Signal(SIGHUP, HandleSignalRestart); + Signal(SIGTERM, HandleSignalTerminate); + builder.Build().Run(); + } + + /// + /// Configures the Pode service for Windows, enabling pause and continue support. + /// + /// The host builder. + /// The name of the service. + [SupportedOSPlatform("windows")] + private static void ConfigureWindows(IHostBuilder builder, string serviceName) + { + using var host = builder.Build(); + var service = new PodeMonitorWindowsService(host, serviceName); + ServiceBase.Run(service); + } + + private static void HandleSignalStop(int signum) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received."); + HandlePause(); + } + + private static void HandleSignalTerminate(int signum) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTERM received."); + HandleStop(); + } + + private static void HandleSignalContinue(int signum) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGCONT received."); + HandleContinue(); + } + + private static void HandleSignalRestart(int signum) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGHUP received."); + HandleRestart(); + } + + private static void HandlePause() => _workerInstance?.OnPause(); + private static void HandleContinue() => _workerInstance?.OnContinue(); + private static void HandleRestart() => _workerInstance?.Restart(); + private static void HandleStop() => _workerInstance?.Shutdown(); +#else + [SupportedOSPlatform("linux")] + private static void ConfigureLinux(IHostBuilder builder) => builder.UseSystemd().Build().Run(); + + [SupportedOSPlatform("macOS")] + private static void ConfigureMacOS(IHostBuilder builder) => builder.Build().Run(); + + [SupportedOSPlatform("windows")] + private static void ConfigureWindows(IHostBuilder builder, string serviceName) => + builder.UseWindowsService().Build().Run(); +#endif + } +} diff --git a/src/PodeMonitor/PodeMonitorServiceState.cs b/src/PodeMonitor/PodeMonitorServiceState.cs new file mode 100644 index 000000000..43e4e1ffd --- /dev/null +++ b/src/PodeMonitor/PodeMonitorServiceState.cs @@ -0,0 +1,51 @@ +namespace PodeMonitor +{ + /// + /// Enum representing possible states of the Pode service. + /// + public enum PodeMonitorServiceState + { + Unknown, // State is unknown + + /// + /// The server has been completely Stopped and is no longer running. + /// + Stopped, + + /// + /// The server is in the process of Stopping and shutting down its operations. + /// + Stopping, + + /// + /// 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 + } + +} \ No newline at end of file diff --git a/src/PodeMonitor/PodeMonitorWindowsService.cs b/src/PodeMonitor/PodeMonitorWindowsService.cs new file mode 100644 index 000000000..b962e3c04 --- /dev/null +++ b/src/PodeMonitor/PodeMonitorWindowsService.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; +using System.ServiceProcess; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Runtime.Versioning; +using System.Diagnostics; + +namespace PodeMonitor +{ + /// + /// Represents a Windows service that integrates with a Pode host and supports lifecycle operations such as start, stop, pause, continue, and restart. + /// + [SupportedOSPlatform("windows")] + public class PodeMonitorWindowsService : ServiceBase + { + private readonly IHost _host; // The Pode host instance + private const int CustomCommandRestart = 128; // Custom command for SIGHUP-like restart + + /// + /// Initializes a new instance of the PodeMonitorWindowsService class. + /// + /// The host instance managing the Pode application. + /// The name of the Windows service. + public PodeMonitorWindowsService(IHost host, string serviceName) + { + _host = host ?? throw new ArgumentNullException(nameof(host), "Host cannot be null."); + CanPauseAndContinue = true; // Enable support for pause and continue operations + ServiceName = serviceName ?? throw new ArgumentNullException(nameof(serviceName), "Service name cannot be null."); // Dynamically set the service name + } + + /// + /// Handles the service start operation. Initializes the Pode host and starts its execution. + /// + /// Command-line arguments passed to the service. + protected override void OnStart(string[] args) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service starting..."); + try + { + base.OnStart(args); // Call the base implementation + _host.StartAsync().Wait(); // Start the Pode host asynchronously and wait for it to complete + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service started successfully."); + } + catch (Exception ex) + { + // Log the exception to the custom log + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Service startup failed."); + + // Write critical errors to the Windows Event Log + EventLog.WriteEntry(ServiceName, $"Critical failure during service startup: {ex.Message}\n{ex.StackTrace}", + EventLogEntryType.Error); + + // Rethrow the exception to signal failure to the Windows Service Manager + throw; + } + } + + /// + /// Handles the service stop operation. Gracefully stops the Pode host. + /// + protected override void OnStop() + { + base.OnStop(); // Call the base implementation + _host.StopAsync().Wait(); // Stop the Pode host asynchronously and wait for it to complete + } + + /// + /// Handles the service pause operation. Pauses the Pode host by invoking IPausableHostedService. + /// + protected override void OnPause() + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service pausing..."); + base.OnPause(); // Call the base implementation + + // Retrieve the IPausableHostedService instance from the service container + var service = _host.Services.GetService(typeof(IPausableHostedService)); + if (service != null) + { + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnPause(); // Invoke the pause operation + } + else + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); + } + } + + /// + /// Handles the service resume operation. Resumes the Pode host by invoking IPausableHostedService. + /// + protected override void OnContinue() + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service resuming..."); + base.OnContinue(); // Call the base implementation + + // Retrieve the IPausableHostedService instance from the service container + var service = _host.Services.GetService(typeof(IPausableHostedService)); + if (service != null) + { + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnContinue(); // Invoke the resume operation + } + else + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); + } + } + + /// + /// Handles custom control commands sent to the service. Supports a SIGHUP-like restart operation. + /// + /// The custom command number. + protected override void OnCustomCommand(int command) + { + if (command == CustomCommandRestart) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Custom restart command received. Restarting service..."); + var service = _host.Services.GetService(typeof(IPausableHostedService)); + if (service != null) + { + ((IPausableHostedService)service).Restart(); // Trigger the restart operation + } + else + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService for restart."); + } + } + else + { + base.OnCustomCommand(command); // Handle other custom commands + } + } + } +} diff --git a/src/PodeMonitor/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs new file mode 100644 index 000000000..89dca3ac9 --- /dev/null +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -0,0 +1,226 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace PodeMonitor +{ + /// + /// Manages the lifecycle of the Pode PowerShell process, supporting start, stop, pause, and resume operations. + /// Implements IPausableHostedService for handling pause and resume operations. + /// + public sealed class PodeMonitorWorker : BackgroundService, IPausableHostedService + { + // Logger instance for logging informational and error messages + private readonly ILogger _logger; + + // Instance of PodeMonitor to manage the Pode PowerShell process + private readonly PodeMonitor _pwshMonitor; + + // Delay in milliseconds to prevent rapid consecutive operations + private readonly int _delayMs = 5000; + + private bool _terminating = false; + + public PodeMonitorServiceState State => _pwshMonitor.State; + + + /// + /// Initializes a new instance of the PodeMonitorWorker class. + /// + /// Logger instance for logging messages and errors. + /// Instance of PodeMonitor for managing the PowerShell process. + public PodeMonitorWorker(ILogger logger, PodeMonitor pwshMonitor) + { + _logger = logger; // Assign the logger + _pwshMonitor = pwshMonitor; // Assign the shared PodeMonitor instance + _logger.LogInformation("PodeMonitorWorker initialized with shared PodeMonitor."); + } + + /// + /// The main execution loop for the worker. + /// Monitors and restarts the Pode PowerShell process if needed. + /// + /// Cancellation token to signal when the worker should stop. + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "PodeMonitorWorker running at: {0}", DateTimeOffset.Now); + int retryCount = 0; // Tracks the number of retries in case of failures + + while (!stoppingToken.IsCancellationRequested && !_terminating) + { + try + { + retryCount = 0; // Reset retry count on success + + // Start the Pode PowerShell process + _pwshMonitor.StartPowerShellProcess(); + } + catch (Exception ex) + { + retryCount++; + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); + + // If retries exceed the maximum, log and exit the loop + if (retryCount >= _pwshMonitor.StartMaxRetryCount) + { + PodeMonitorLogger.Log(PodeLogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + break; + } + + // Delay before retrying + await Task.Delay(_pwshMonitor.StartRetryDelayMs, stoppingToken); + } + + // Add a delay between iterations + await Task.Delay(10000, stoppingToken); + } + + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Monitoring loop has stopped."); + } + + /// + /// Stops the Pode PowerShell process gracefully. + /// + /// Cancellation token to signal when the stop should occur. + public override async Task StopAsync(CancellationToken stoppingToken) + { + Shutdown(); + + await base.StopAsync(stoppingToken); // Wait for the base StopAsync to complete + + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); + } + + + /// + /// Shutdown the Pode PowerShell process by sending a shutdown command. + /// + public void Shutdown() + { + if ((!_terminating) && (_pwshMonitor.State == PodeMonitorServiceState.Running || _pwshMonitor.State == PodeMonitorServiceState.Suspended)) + { + + _terminating = true; + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); + + try + { + _pwshMonitor.StopPowerShellProcess(); // Stop the process + + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Stop message sent via pipe at: {0}", DateTimeOffset.Now); + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); + } + } + } + + /// + /// Restarts the Pode PowerShell process by sending a restart command. + /// + public void Restart() + { + if ((!_terminating) && _pwshMonitor.State == PodeMonitorServiceState.Running || _pwshMonitor.State == PodeMonitorServiceState.Suspended) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); + try + { + _pwshMonitor.RestartPowerShellProcess(); // Restart the process + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + + //AddOperationDelay("Pause"); // Delay to ensure stability + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); + } + } + } + + /// + /// Pauses the Pode PowerShell process and adds a delay to ensure stable operation. + /// + public void OnPause() + { + if ((!_terminating) && _pwshMonitor.State == PodeMonitorServiceState.Running) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); + + try + { + _pwshMonitor.SuspendPowerShellProcess(); // Send pause command to the process + + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + var retryCount = 0; // Reset retry count on success + while (_pwshMonitor.State != PodeMonitorServiceState.Suspended) + { + if (retryCount >= 100) + { + PodeMonitorLogger.Log(PodeLogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + break; + } + + // Delay before retrying + Thread.Sleep(200); + } + //AddOperationDelay("Pause"); // Delay to ensure stability + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); + } + } + } + + /// + /// Resumes the Pode PowerShell process and adds a delay to ensure stable operation. + /// + public void OnContinue() + { + if ((!_terminating) && _pwshMonitor.State == PodeMonitorServiceState.Suspended) + { + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); + + try + { + _pwshMonitor.ResumePowerShellProcess(); // Send resume command to the process + + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); + var retryCount = 0; // Reset retry count on success + while (_pwshMonitor.State == PodeMonitorServiceState.Suspended) + { + if (retryCount >= 100) + { + PodeMonitorLogger.Log(PodeLogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + break; + } + + // Delay before retrying + Thread.Sleep(200); + } + + + // AddOperationDelay("Resume"); // Delay to ensure stability + } + catch (Exception ex) + { + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error during continue: {0}", ex.Message); + } + } + } + + /// + /// Adds a delay to ensure that rapid consecutive operations are prevented. + /// + /// The name of the operation (e.g., "Pause" or "Resume"). + private void AddOperationDelay(string operation) + { + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, "{0} operation completed. Adding delay of {1} ms.", operation, _delayMs); + Thread.Sleep(_delayMs); // Introduce a delay + } + } +} diff --git a/src/PodeMonitor/PodeMonitorWorkerOptions.cs b/src/PodeMonitor/PodeMonitorWorkerOptions.cs new file mode 100644 index 000000000..5849d85a8 --- /dev/null +++ b/src/PodeMonitor/PodeMonitorWorkerOptions.cs @@ -0,0 +1,109 @@ +using System; + +namespace PodeMonitor +{ + /// + /// Configuration options for the PodeMonitorWorker service. + /// These options determine how the worker operates, including paths, parameters, and retry policies. + /// + public class PodeMonitorWorkerOptions + { + /// + /// The name of the service. + /// + public string Name { get; set; } + + /// + /// The path to the PowerShell script that the worker will execute. + /// + public string ScriptPath { get; set; } + + /// + /// The path to the PowerShell executable (pwsh). + /// + public string PwshPath { get; set; } + + /// + /// Additional parameters to pass to the PowerShell process. + /// Default is an empty string. + /// + public string ParameterString { get; set; } = ""; + + /// + /// The path to the log file where output from the PowerShell process will be written. + /// Default is an empty string (no logging). + /// + public string LogFilePath { get; set; } = ""; + + /// + /// The logging level for the service (e.g., DEBUG, INFO, WARN, ERROR, CRITICAL). + /// Default is INFO. + /// + public PodeLogLevel LogLevel { get; set; } = PodeLogLevel.INFO; + + /// + /// The maximum size (in bytes) of the log file before it is rotated. + /// Default is 10 MB (10 * 1024 * 1024 bytes). + /// + public long LogMaxFileSize { get; set; } = 10 * 1024 * 1024; + + /// + /// Indicates whether the PowerShell process should run in quiet mode, suppressing output. + /// Default is true. + /// + public bool Quiet { get; set; } = true; + + /// + /// Indicates whether termination of the PowerShell process is disabled. + /// Default is true. + /// + public bool DisableTermination { get; set; } = true; + + /// + /// The maximum time to wait (in milliseconds) for the PowerShell process to shut down. + /// Default is 30,000 milliseconds (30 seconds). + /// + public int ShutdownWaitTimeMs { get; set; } = 30000; + + /// + /// The maximum number of retries to start the PowerShell process before giving up. + /// Default is 3 retries. + /// + public int StartMaxRetryCount { get; set; } = 3; + + /// + /// The delay (in milliseconds) between retry attempts to start the PowerShell process. + /// Default is 5,000 milliseconds (5 seconds). + /// + public int StartRetryDelayMs { get; set; } = 5000; + + /// + /// Disables all console interactions for the server. + /// + public bool DisableConsoleInput { get; set; } = true; + + /// + /// Prevents the server from loading settings from the server.psd1 configuration file. + /// + public bool IgnoreServerConfig { get; set; } = false; + + /// + /// Specifies a custom configuration file instead of using the default `server.psd1`. + /// + public string ConfigFile { get; set; } = ""; + + /// + /// Provides a string representation of the configured options for debugging or logging purposes. + /// + /// A string containing all configured options and their values. + public override string ToString() + { + return $"Name: {Name}, ScriptPath: {ScriptPath}, PwshPath: {PwshPath}, ParameterString: {ParameterString}, " + + $"LogFilePath: {LogFilePath}, LogLevel: {LogLevel}, LogMaxFileSize: {LogMaxFileSize}, Quiet: {Quiet}, " + + $"DisableTermination: {DisableTermination}, ShutdownWaitTimeMs: {ShutdownWaitTimeMs}, " + + $"StartMaxRetryCount: {StartMaxRetryCount}, StartRetryDelayMs: {StartRetryDelayMs}" + + $"DisableConsoleInput: {IgnoreServerConfig}, IgnoreServerConfig: {IgnoreServerConfig}, ConfigFile: {ConfigFile}"; + } + } + +} diff --git a/src/PodeMonitor/PodeServiceStateExtensions.cs b/src/PodeMonitor/PodeServiceStateExtensions.cs new file mode 100644 index 000000000..21096e295 --- /dev/null +++ b/src/PodeMonitor/PodeServiceStateExtensions.cs @@ -0,0 +1,46 @@ +using System; + +namespace PodeMonitor +{ + public static class PodeServiceStateExtensions + { + /// + /// Converts a string to a PodeMonitorServiceState enum in a case-insensitive manner. + /// + /// The string representation of the state. + /// The corresponding PodeMonitorServiceState, or Unknown if parsing fails. + public static PodeMonitorServiceState ToPodeMonitorServiceState(this string stateString) + { + if (string.IsNullOrWhiteSpace(stateString)) + return PodeMonitorServiceState.Unknown; + + // Normalize known aliases + stateString = stateString.Trim().ToLowerInvariant(); + switch (stateString) + { + case "terminated": + return PodeMonitorServiceState.Stopped; + case "terminating": + return PodeMonitorServiceState.Stopping; + } + + // Try parsing the string to an enum + if (Enum.TryParse(stateString, true, out PodeMonitorServiceState result)) + { + return result; + } + + return PodeMonitorServiceState.Unknown; // Default if parsing fails + } + + /// + /// Converts a PodeMonitorServiceState enum to its string representation. + /// + /// The PodeMonitorServiceState enum value. + /// The string representation of the state. + public static string ToPodeMonitorServiceStateString(this PodeMonitorServiceState state) + { + return state.ToString(); + } + } +} diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index a4f4f9d98..454a93ba7 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -53,7 +53,10 @@ function New-PodeContext { $IgnoreServerConfig, [string] - $ConfigFile + $ConfigFile, + + [hashtable] + $Service ) # set a random server name if one not supplied @@ -98,6 +101,10 @@ function New-PodeContext { $ctx.Server.Console = $Console $ctx.Server.ComputerName = [System.Net.DNS]::GetHostName() + + if ($null -ne $Service) { + $ctx.Server.Service = $Service + } # list of created listeners/receivers $ctx.Listeners = @() $ctx.Receivers = @() @@ -146,6 +153,7 @@ function New-PodeContext { Tasks = 2 WebSockets = 2 Timers = 1 + Service = 0 } # set socket details for pode server @@ -214,9 +222,11 @@ function New-PodeContext { # 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) { @{} } + if ($IgnoreServerConfig) { + $ctx.Server.Configuration = @{} + } else { - Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx -ConfigFile $ConfigFile + $ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx -ConfigFile $ConfigFile } # Set the 'Enabled' property of the server configuration. @@ -500,6 +510,7 @@ function New-PodeContext { Tasks = $null Files = $null Timers = $null + Service = $null } # threading locks, etc. @@ -706,6 +717,15 @@ function New-PodeRunspacePool { $PodeContext.RunspacePools.Gui.Pool.ApartmentState = 'STA' } + + if (Test-PodeServiceEnabled ) { + $PodeContext.Threads['Service'] = 1 + $PodeContext.RunspacePools.Service = @{ + Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host) + State = 'Waiting' + LastId = 0 + } + } } <# diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index b2a32e66c..898fc54ba 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -145,27 +145,6 @@ function Get-PodePSVersionTable { return $PSVersionTable } -function Test-PodeIsAdminUser { - # check the current platform, if it's unix then return true - if (Test-PodeIsUnix) { - return $true - } - - try { - $principal = [System.Security.Principal.WindowsPrincipal]::new([System.Security.Principal.WindowsIdentity]::GetCurrent()) - if ($null -eq $principal) { - return $false - } - - return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) - } - catch [exception] { - Write-PodeHost 'Error checking user administrator priviledges' -ForegroundColor Red - Write-PodeHost $_.Exception.Message -ForegroundColor Red - return $false - } -} - function Get-PodeHostIPRegex { param( [Parameter(Mandatory = $true)] @@ -3738,10 +3717,10 @@ function Resolve-PodeObjectArray { # Changes to $clonedObject will not affect $originalObject and vice versa. .NOTES - This function uses the System.Management.Automation.PSSerializer class, which is available in - PowerShell 5.1 and later versions. The default depth parameter is set to 10 to handle nested - objects appropriately, but it can be customized via the -Deep parameter. - This is an internal function and may change in future releases of Pode. + - This function uses the System.Management.Automation.PSSerializer class, which is available in + PowerShell 5.1 and later versions. The default depth parameter is set to 10 to handle nested + objects appropriately, but it can be customized via the -Deep parameter. + - This is an internal function and may change in future releases of Pode. #> function Copy-PodeObjectDeepClone { param ( @@ -3942,6 +3921,262 @@ function Test-PodeIsISEHost { return ((Test-PodeIsWindows) -and ('Windows PowerShell ISE Host' -eq $Host.Name)) } + +<# +.SYNOPSIS + Tests if the current user has administrative privileges on Windows or root/sudo privileges on Linux/macOS. + +.DESCRIPTION + This function checks the current user's privileges. On Windows, it checks if the user is an Administrator. + If the session is not elevated, you can optionally check if the user has the potential to elevate using the -Elevate switch. + On Linux and macOS, it checks if the user is either root or has sudo (Linux) or admin (macOS) privileges. + You can also check if the user has the potential to elevate by belonging to the sudo or admin group using the -Elevate switch. + +.PARAMETER Elevate + The -Elevate switch allows you to check if the current user has the potential to elevate to administrator/root privileges, + even if the session is not currently elevated. + +.PARAMETER Console + The -Console switch will output errors to the console if an exception occurs. + Otherwise, the errors will be written to the Pode error log. + +.EXAMPLE + Test-PodeAdminPrivilege + + If the user has administrative privileges, it returns $true. If not, it returns $false. + +.EXAMPLE + Test-PodeAdminPrivilege -Elevate + + This will check if the user has administrative/root/sudo privileges or the potential to elevate, + even if the session is not currently elevated. + +.EXAMPLE + Test-PodeAdminPrivilege -Elevate -Console + + This will check for admin privileges or potential to elevate and will output errors to the console if any occur. + +.OUTPUTS + [bool] + Returns $true if the user has administrative/root/sudo/admin privileges or the potential to elevate, + otherwise returns $false. + +.NOTES + - This function works across multiple platforms: Windows, Linux, and macOS. + On Linux/macOS, it checks for root, sudo, or admin group memberships, and optionally checks for elevation potential + if the -Elevate switch is used. + - This is an internal function and may change in future releases of Pode. +#> + +function Test-PodeAdminPrivilege { + param( + [switch] + $Elevate, + [switch] + $Console + ) + try { + # Check if the operating system is Windows + if (Test-PodeIsWindows) { + + # Retrieve the current Windows identity and token + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + + if ($null -eq $principal) { + return $false + } + + $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if ($isAdmin) { + return $true + } + + # Check if the token is elevated + if ($identity.IsSystem -or $identity.IsAuthenticated -and $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + return $true + } + + if ($Elevate.IsPresent) { + # Use 'whoami /groups' to check if the user has the potential to elevate + $groups = whoami /groups + if ($groups -match 'S-1-5-32-544') { + return $true + } + } + return $false + } + else { + # Check if the operating system is Linux or macOS (both are Unix-like) + + # Check if the user is root (UID 0) + $isRoot = [int](id -u) + if ($isRoot -eq 0) { + return $true + } + + if ($Elevate.IsPresent) { + # Check if the user has sudo privileges by checking sudo group membership + $user = whoami + $groups = (groups $user) + Write-Verbose "User:$user Groups: $( $groups -join ',')" + # macOS typically uses 'admin' group for sudo privileges + return ($groups -match '\bwheel\b' -or $groups -match '\badmin\b' -or $groups -match '\bsudo\b' -or $groups -match '\badm\b') + } + return $false + } + } + catch [exception] { + if ($Console.IsPresent) { + Write-PodeHost 'Error checking user privileges' -ForegroundColor Red + Write-PodeHost $_.Exception.Message -ForegroundColor Red + } + else { + $_ | Write-PodeErrorLog + } + return $false + } +} + +<# +.SYNOPSIS + Starts a command with elevated privileges if the current session is not already elevated. + +.DESCRIPTION + This function checks if the current PowerShell session is running with administrator privileges. + If not, it re-launches the command as an elevated process. If the session is already elevated, + it will execute the command directly and return the result of the command. + +.PARAMETER Command + The PowerShell command to be executed. This can be any valid PowerShell command, script, or executable. + +.PARAMETER Arguments + The arguments to be passed to the command. This can be any valid argument list for the command or script. + +.EXAMPLE + Invoke-PodeWinElevatedCommand -Command "Get-Service" -Arguments "-Name 'W32Time'" + + This will run the `Get-Service` command with elevated privileges, pass the `-Name 'W32Time'` argument, and return the result. + +.EXAMPLE + Invoke-PodeWinElevatedCommand -Command "C:\Scripts\MyScript.ps1" -Arguments "-Param1 'Value1' -Param2 'Value2'" + + This will run the script `MyScript.ps1` with elevated privileges, pass the parameters `-Param1` and `-Param2`, and return the result. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Invoke-PodeWinElevatedCommand { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] + param ( + [string] + $Command, + [string] + $Arguments, + [PSCredential] $Credential + ) + + + # Check if the current session is elevated + $isElevated = ([Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + + + if (-not $isElevated) { + + # Escape the arguments by replacing " with `" (escaping quotes) + $escapedArguments = $Arguments -replace '"', '"""' + $psCredential = '' + + # Combine command and arguments into a string to pass for elevated execution + # $escapedCommand = "`"$Command`" $Arguments" + if ($Credential) { + $password = Convertfrom-SecureString $Credential.Password + $psCredential = "-Credential ([pscredential]::new('$($Credential.UserName)', `$('$password'|ConvertTo-SecureString)))" + } + + # Combine command and arguments into a string for elevated execution + $escapedCommand = "$Command $psCredential $escapedArguments" + # Start elevated process with properly escaped command and arguments + $result = Start-Process -FilePath ((Get-Process -Id $PID).Path) ` + -ArgumentList '-NoProfile', '-ExecutionPolicy Bypass', "-Command & {$escapedCommand}" ` + -Verb RunAs -Wait -PassThru + + return $result + } + + # Run the command directly with arguments if elevated and capture the output + return Invoke-Expression "$Command $Arguments" +} + +<# +.SYNOPSIS + Determines the OS architecture for the current system. + +.DESCRIPTION + This function detects the operating system's architecture and maps it to a format compatible with + PowerShell installation requirements. It works on both Windows and Unix-based systems, translating + various architecture identifiers (e.g., 'amd64', 'x86_64') into standardized PowerShell-supported names + like 'x64', 'x86', 'arm64', and 'arm32'. On Linux, the function also checks for musl libc to provide + an architecture-specific identifier. + +.OUTPUTS + [string] - The architecture string, such as 'x64', 'x86', 'arm64', 'arm32', or 'musl-x64'. + +.EXAMPLE + $arch = Get-PodeOSPwshArchitecture + Write-Host "Current architecture: $arch" + +.NOTES + - For Windows, architecture is derived from the `PROCESSOR_ARCHITECTURE` environment variable. + - For Unix-based systems, architecture is determined using the `uname -m` command. + - The function adds support for identifying musl libc on Linux, returning 'musl-x64' if detected. + - If the architecture is not supported, the function returns an empty string. +#> +function Get-PodeOSPwshArchitecture { + # Initialize an empty variable for storing the detected architecture + $arch = [string]::Empty + + # Detect architecture on Unix-based systems (Linux/macOS) + if ($IsLinux -or $IsMacOS) { + # Use the 'uname -m' command to determine the system architecture + $arch = uname -m + } + else { + # For Windows, use the environment variable 'PROCESSOR_ARCHITECTURE' + $arch = $env:PROCESSOR_ARCHITECTURE + } + + # Map the detected architecture to PowerShell-compatible formats + switch ($arch.ToLowerInvariant()) { + 'amd64' { $arch = 'x64' } # 64-bit AMD architecture + 'x86' { $arch = 'x86' } # 32-bit Intel architecture + 'x86_64' { $arch = 'x64' } # 64-bit Intel architecture + 'armv7*' { $arch = 'arm32' } # 32-bit ARM architecture (v7 series) + 'aarch64*' { $arch = 'arm64' } # 64-bit ARM architecture (aarch64 series) + 'arm64' { $arch = 'arm64' } # Explicit ARM64 + 'arm64*' { $arch = 'arm64' } # Pattern matching for ARM64 + 'armv8*' { $arch = 'arm64' } # ARM v8 series + default { return '' } # Unsupported architectures, return empty string + } + + # Additional check for musl libc on Linux systems + if ($IsLinux) { + if ($arch -eq 'x64') { + # Check if musl libc is present + if (Get-Command ldd -ErrorAction SilentlyContinue) { + $lddOutput = ldd --version 2>&1 + if ($lddOutput -match 'musl') { + # Append 'musl-' prefix to architecture + $arch = 'musl-x64' + } + } + } + } + + # Return the final architecture string + return $arch +} + <# .SYNOPSIS Creates aliases for Pode OpenAPI functions to support legacy naming conventions. @@ -3952,7 +4187,7 @@ function Test-PodeIsISEHost { - Enable-PodeOA as an alias for Enable-PodeOpenApi. - Get-PodeOpenApiDefinition as an alias for Get-PodeOADefinition. The function helps maintain backward compatibility and simplifies calling Pode OpenAPI functions. - + .PARAMETER None This function does not accept any parameters. .OUTPUTS diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index 3b35edb35..57e5cd128 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -135,7 +135,7 @@ function ConvertTo-PodeEventViewerLevel { function Get-PodeLoggingInbuiltType { param( [Parameter(Mandatory = $true)] - [ValidateSet('Errors', 'Requests')] + [ValidateSet('Errors', 'Requests','service')] [string] $Type ) @@ -191,6 +191,34 @@ function Get-PodeLoggingInbuiltType { "StackTrace: $($item.StackTrace)" ) + # join the details and return + return "$($row -join "`n")`n" + } + } + 'service' { + $script = { + param($item, $options) + + # do nothing if the error level isn't present + if (@($options.Levels) -inotcontains $item.Level) { + return + } + + # just return the item if Raw is set + if ($options.Raw) { + return $item + } + + # build the exception details + $row = @( + "Date: $($item.Date.ToString('yyyy-MM-dd HH:mm:ss'))", + "Level: $($item.Level)", + "ThreadId: $($item.ThreadId)", + "Server: $($item.Server)", + "Category: $($item.Category)", + "Message: $($item.Message)" + ) + # join the details and return return "$($row -join "`n")`n" } diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index da4bd0b46..b7845273a 100644 --- a/src/Private/Runspaces.ps1 +++ b/src/Private/Runspaces.ps1 @@ -42,7 +42,6 @@ function Add-PodeRunspace { param( [Parameter(Mandatory = $true)] - [ValidateSet('Main', 'Signals', 'Schedules', 'Gui', 'Web', 'Smtp', 'Tcp', 'Tasks', 'WebSockets', 'Files', 'Timers')] [string] $Type, diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index c2051a90c..9573ba06e 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -196,6 +196,8 @@ function Start-PodeInternalServer { Invoke-PodeEvent -Type Running + # Start Service Monitor + Start-PodeServiceHeartbeat } catch { throw diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 new file mode 100644 index 000000000..3cff5daca --- /dev/null +++ b/src/Private/Service.ps1 @@ -0,0 +1,1644 @@ +<# +.SYNOPSIS + Tests if the Pode service is enabled. + +.DESCRIPTION + This function checks if the Pode service is enabled by verifying if the `Service` key exists in the `$PodeContext.Server` hashtable. + +.OUTPUTS + [Bool] - `$true` if the 'Service' key exists, `$false` if it does not. + +.EXAMPLE + Test-PodeServiceEnabled + + Returns `$true` if the Pode service is enabled, otherwise returns `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeServiceEnabled { + + # Check if the 'Service' key exists in the $PodeContext.Server hashtable + return $PodeContext.Server.ContainsKey('Service') +} + +<# +.SYNOPSIS + Starts the Pode Service Heartbeat using a named pipe for communication with a C# service. + +.DESCRIPTION + This function starts a named pipe server in PowerShell that listens for commands from a C# application. It supports two commands: + - 'shutdown': to gracefully stop the Pode server. + - 'restart': to restart the Pode server. + +.PARAMETER None + The function takes no parameters. It retrieves the pipe name from the Pode service context. + +.EXAMPLE + Start-PodeServiceHeartbeat + + This command starts the Pode service monitoring and waits for 'shutdown' or 'restart' commands from the named pipe. + +.NOTES + This is an internal function and may change in future releases of Pode. + + The function uses Pode's context for the service to manage the pipe server. The pipe listens for messages sent from a C# client + and performs actions based on the received message. + + If the pipe receives a 'stop' message, the Pode server is stopped. + If the pipe receives a 'restart' message, the Pode server is restarted. + + Global variable example: $global:PodeService=@{DisableTermination=$true;Quiet=$false;Pipename='ssss'} +#> +function Start-PodeServiceHeartbeat { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + [CmdletBinding()] + param() + # Check if the Pode service is enabled + if (Test-PodeServiceEnabled) { + + # Define the script block for the client receiver, listens for commands via the named pipe + $scriptBlock = { + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { + + do { + Start-Sleep -Seconds 1 + $serviceState = Get-PodeServerState + }until(( [Pode.PodeServerState]::Running, [Pode.PodeServerState]::Suspended, [Pode.PodeServerState]::Terminating) -contains ( $serviceState) ) + + [System.Console]::WriteLine("Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)") + [System.Console]::WriteLine("Service State: $serviceState") + [System.Console]::WriteLine("Total Uptime: $(Get-PodeServerUptime -Total -Format verbose -ExcludeMilliseconds)") + if ((Get-PodeServerUptime) -gt 1000) { + [System.Console]::WriteLine("Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)") + } + [System.Console]::WriteLine("Total Number of Restart: $(Get-PodeServerRestartCount)") + try { + Start-Sleep -Milliseconds 100 + # Create a named pipe server stream + $pipeStream = [System.IO.Pipes.NamedPipeServerStream]::new( + $PodeContext.Server.Service.PipeName, + [System.IO.Pipes.PipeDirection]::InOut, + 1, # Max number of allowed concurrent connections + [System.IO.Pipes.PipeTransmissionMode]::Byte, + [System.IO.Pipes.PipeOptions]::None + ) + + [System.Console]::WriteLine("Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe.") + $pipeStream.WaitForConnection() # Wait until a client connects + [System.Console]::WriteLine("Connected to the $($PodeContext.Server.Service.PipeName) pipe.") + + # Create a StreamReader to read incoming messages from the pipe + $reader = [System.IO.StreamReader]::new($pipeStream) + + # Process incoming messages in a loop as long as the pipe is connected + if ($pipeStream.IsConnected) { + $message = $reader.ReadLine() # Read message from the pipe + if ( Test-PodeCancellationTokenRequest -Type Terminate) { + return + } + + if ($message) { + [System.Console]::WriteLine("Received message: $message") + + switch ($message) { + 'stop' { + # Process 'shutdown' message + [System.Console]::WriteLine("Server request: 'Stop'. Closing Pode ...") + Close-PodeServer # Gracefully stop Pode server + Start-Sleep 1 + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") + + [System.Console]::WriteLine('Closing Service Monitoring Heartbeat') + return # Exit the loop + } + + 'restart' { + # Process 'restart' message + [System.Console]::WriteLine("Server request: 'Restart'. Restarting Pode ...") + Restart-PodeServer # Restart Pode server + Start-Sleep 1 + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") + + [System.Console]::WriteLine('Closing Service Monitoring Heartbeat') + return # Exit the loop + } + + 'suspend' { + # Process 'suspend' message + [System.Console]::WriteLine("Server request: 'Suspend'. Suspending Pode ...") + Suspend-PodeServer + Start-Sleep 1 + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") + break + } + + 'resume' { + # Process 'resume' message + [System.Console]::WriteLine("Server request: 'Resume'. Resuming Pode ...") + Resume-PodeServer + Start-Sleep 1 + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") + break + } + } + + } + } + } + catch { + $_ | Write-PodeErrorLog # Log any errors that occur during pipe operation + } + finally { + if ($reader) { + $reader.Dispose() + } + if ($pipeStream) { + $pipeStream.Flush() + $pipeStream.Close() + $pipeStream.Dispose() # Always dispose of the pipe stream when done + [System.Console]::WriteLine("Disposing Listener Pipe $($PodeContext.Server.Service.PipeName)") + } + } + + } + [System.Console]::WriteLine('Closing Service Monitoring Heartbeat') + } + + # Assign a name to the Pode service + $PodeContext.Server.Service['Name'] = 'Service' + Write-Verbose -Message 'Starting service monitoring' + + # Start the runspace that runs the client receiver script block + $PodeContext.Server.Service['Runspace'] = Add-PodeRunspace -Type 'Service' -ScriptBlock ($scriptBlock) -PassThru + } +} + +<# +.SYNOPSIS + Registers a Pode service as a macOS LaunchAgent/Daemon. + +.DESCRIPTION + The `Register-PodeMacService` function creates a macOS plist file for the Pode service. It sets up the service + to run using `launchctl`, specifying options such as autostart, logging, and the executable path. + +.PARAMETER Name + The name of the Pode service. This is used to identify the service in macOS. + +.PARAMETER Description + A brief description of the service. This is not included in the plist file but can be useful for logging. + +.PARAMETER BinPath + The path to the directory where the PodeMonitor executable is located. + +.PARAMETER SettingsFile + The path to the configuration file (e.g., `srvsettings.json`) that the Pode service will use. + +.PARAMETER User + The user under which the Pode service will run. + +.PARAMETER Start + If specified, the service will be started after registration. + +.PARAMETER Autostart + If specified, the service will automatically start when the system boots. + +.PARAMETER OsArchitecture + Specifies the architecture of the operating system (e.g., `osx-x64` or `osx-arm64`). + +.PARAMETER Agent + A switch to create an Agent instead of a Daemon in MacOS. + +.OUTPUTS + Returns $true if successful. + +.EXAMPLE + Register-PodeMacService -Name 'MyPodeService' -Description 'My Pode service' -BinPath '/path/to/bin' ` + -SettingsFile '/path/to/srvsettings.json' -User 'podeuser' -Start -Autostart -OsArchitecture 'osx-arm64' + + Registers a Pode service on macOS and starts it immediately with autostart enabled. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Register-PodeMacService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [string] + $Description, + + [string] + $BinPath, + + [string] + $SettingsFile, + + [string] + $User, + + [string] + $OsArchitecture, + + [string] + $LogPath, + + [switch] + $Agent + ) + + $nameService = Get-PodeRealServiceName -Name $Name + + # Check if the service is already registered + if ((Test-PodeMacOsServiceIsRegistered $nameService -Agent:$Agent)) { + # Service is already registered. + throw ($PodeLocale.serviceAlreadyRegisteredException -f $nameService) + } + + # Determine whether the service should run at load + $runAtLoad = if ($Autostart.IsPresent) { '' } else { '' } + + + # Create a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + + # Create the plist content + @" + + + + + Label + $nameService + + ProgramArguments + + $BinPath/$OsArchitecture/PodeMonitor + $SettingsFile + + + WorkingDirectory + $BinPath + + RunAtLoad + $runAtLoad + + StandardOutPath + $LogPath/$nameService.stdout.log + + StandardErrorPath + $LogPath/$nameService.stderr.log + + KeepAlive + + SuccessfulExit + + + + + + +"@ | Set-Content -Path $tempFile -Encoding UTF8 + + Write-Verbose -Message "Service '$nameService' WorkingDirectory : $($BinPath)." + try { + if ($Agent) { + $plistPath = "$($HOME)/Library/LaunchAgents/$($nameService).plist" + Copy-Item -Path $tempFile -Destination $plistPath + #set rw r r permissions + chmod 644 $plistPath + # Load the plist with launchctl + launchctl load $plistPath + } + else { + $plistPath = "/Library/LaunchDaemons/$($nameService).plist" + & sudo cp $tempFile $plistPath + #set rw r r permissions + & sudo chmod 644 $plistPath + + & sudo chown root:wheel $plistPath + + # Load the plist with launchctl + & sudo launchctl load $plistPath + + } + + # Verify the service is now registered + if (! (Test-PodeMacOsServiceIsRegistered $nameService -Agent:$Agent)) { + # Service registration failed. + throw ($PodeLocale.serviceRegistrationException -f $nameService) + } + } + catch { + $_ | Write-PodeErrorLog + throw $_ # Rethrow the error after logging + } + + return $true +} + + +<# +.SYNOPSIS + Registers a new systemd service on a Linux system to run a Pode-based PowerShell worker. + +.DESCRIPTION + The `Register-PodeLinuxService` function configures and registers a new systemd service on a Linux system. + It sets up the service with the specified parameters, generates the service definition file, enables the service, + and optionally starts it. It can also create the necessary user if it does not exist. + +.PARAMETER Name + The name of the systemd service to be registered. + +.PARAMETER Description + A brief description of the service. Defaults to an empty string. + +.PARAMETER BinPath + The path to the directory containing the `PodeMonitor` executable. + +.PARAMETER SettingsFile + The path to the settings file for the Pode worker. + +.PARAMETER User + The name of the user under which the service will run. If the user does not exist, it will be created unless the `SkipUserCreation` switch is used. + +.PARAMETER Group + The group under which the service will run. Defaults to the same as the `User` parameter. + +.PARAMETER OsArchitecture + The architecture of the operating system (e.g., `x64`, `arm64`). Used to locate the appropriate binary. + +.OUTPUTS + Returns $true if successful. + +.EXAMPLE + Register-PodeLinuxService -Name "PodeExampleService" -Description "An example Pode service" ` + -BinPath "/usr/local/bin" -SettingsFile "/etc/pode/example-settings.json" ` + -User "podeuser" -Group "podegroup" -Start -OsArchitecture "x64" + + Registers a new systemd service named "PodeExampleService", creates the necessary user and group, + generates the service file, enables the service, and starts it. + +.EXAMPLE + Register-PodeLinuxService -Name "PodeExampleService" -BinPath "/usr/local/bin" ` + -SettingsFile "/etc/pode/example-settings.json" -User "podeuser" -SkipUserCreation ` + -OsArchitecture "arm64" + + Registers a new systemd service without creating the user, and does not start the service immediately. + +.NOTES + - This function assumes systemd is the init system on the Linux machine. + - The function will check if the service is already registered and will throw an error if it is. + - If the user specified by the `User` parameter does not exist, the function will create it unless the `SkipUserCreation` switch is used. + - This is an internal function and may change in future releases of Pode. +#> +function Register-PodeLinuxService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [string] + $Description, + + [string] + $BinPath, + + [string] + $SettingsFile, + + [string] + $User, + + [string] + $Group, + + [switch] + $Start, + + [string] + $OsArchitecture + ) + $nameService = Get-PodeRealServiceName -Name $Name + $null = systemctl status $nameService 2>&1 + + # Check if the service is already registered + if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { + # Service is already registered. + throw ($PodeLocale.serviceAlreadyRegisteredException -f $nameService ) + } + # Create a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + + $execStart = "$BinPath/$OsArchitecture/PodeMonitor `"$SettingsFile`"" + # Create the service file + @" +[Unit] +Description=$Description +After=network.target + +[Service] +ExecStart=$execStart +WorkingDirectory=$BinPath +Restart=always +User=$User +KillMode=process +Environment=NOTIFY_SOCKET=/run/systemd/notify +Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1 +# Uncomment and adjust if needed +# Group=$Group +# Environment=ASPNETCORE_ENVIRONMENT=Production + +[Install] +WantedBy=multi-user.target +"@ | Set-Content -Path $tempFile -Encoding UTF8 + + Write-Verbose -Message "Service '$nameService' ExecStart : $execStart)." + + & sudo cp $tempFile "/etc/systemd/system/$nameService" + + Remove-Item -path $tempFile -ErrorAction SilentlyContinue + + # Enable the service and check if it fails + try { + if (!(Enable-PodeLinuxService -Name $nameService)) { + # Service registration failed. + throw ($PodeLocale.serviceRegistrationException -f $nameService) + } + } + catch { + $_ | Write-PodeErrorLog + throw $_ # Rethrow the error after logging + return $false + } + + return $true +} + +<# +.SYNOPSIS + Registers a new Windows service to run a Pode-based PowerShell worker. + +.DESCRIPTION + The `Register-PodeMonitorWindowsService` function configures and registers a new Windows service to run a Pode-based PowerShell worker. + It sets up the service with the specified parameters, including paths to the Pode monitor executable, configuration file, + credentials, and security descriptor. The service can be optionally started immediately after registration. + +.PARAMETER Name + The name of the Windows service to be registered. + +.PARAMETER Description + A brief description of the service. Defaults to an empty string. + +.PARAMETER DisplayName + The display name of the service, as it will appear in the Windows Services Manager. + +.PARAMETER StartupType + Specifies how the service is started. Options are: 'Automatic', 'Manual', or 'Disabled'. Defaults to 'Automatic'. + +.PARAMETER BinPath + The path to the directory containing the `PodeMonitor` executable. + +.PARAMETER SettingsFile + The path to the configuration file for the Pode worker. + +.PARAMETER Credential + A `PSCredential` object specifying the credentials for the account under which the service will run. + +.PARAMETER SecurityDescriptorSddl + An SDDL string (Security Descriptor Definition Language) used to define the security of the service. + +.PARAMETER OsArchitecture + The architecture of the operating system (e.g., `x64`, `arm64`). Used to locate the appropriate binary. + +.OUTPUTS + Returns $true if successful. + +.EXAMPLE + Register-PodeMonitorWindowsService -Name "PodeExampleService" -DisplayName "Pode Example Service" ` + -BinPath "C:\Pode" -SettingsFile "C:\Pode\settings.json" ` + -StartupType "Automatic" -Credential (Get-Credential) -Start -OsArchitecture "x64" + + Registers a new Windows service named "PodeExampleService", creates the service with credentials, + generates the service, and starts it. + +.EXAMPLE + Register-PodeMonitorWindowsService -Name "PodeExampleService" -BinPath "C:\Pode" ` + -SettingsFile "C:\Pode\settings.json" -OsArchitecture "x64" + + Registers a new Windows service without credentials or immediate startup. + +.NOTES + - This function assumes the service binary exists at the specified `BinPath`. + - It checks if the service already exists and throws an error if it does. + - This is an internal function and may change in future releases of Pode. +#> + +function Register-PodeMonitorWindowsService { + param( + [string] + $Name, + + [string] + $Description, + + [string] + $DisplayName, + + [string] + $StartupType, + + [string] + $BinPath, + + [string] + $SettingsFile, + + [pscredential] + $Credential, + + [string] + $SecurityDescriptorSddl, + + [string] + $OsArchitecture + ) + + + # Check if service already exists + if (Get-Service -Name $Name -ErrorAction SilentlyContinue) { + # Service is already registered. + throw ($PodeLocale.serviceAlreadyRegisteredException -f "$Name") + + } + + # Parameters for New-Service + $params = @{ + Name = $Name + BinaryPathName = "`"$BinPath\$OsArchitecture\PodeMonitor.exe`" `"$SettingsFile`"" + DisplayName = $DisplayName + StartupType = $StartupType + Description = $Description + #DependsOn = 'NetLogon' + } + if ($SecurityDescriptorSddl) { + $params['SecurityDescriptorSddl'] = $SecurityDescriptorSddl + } + Write-Verbose -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + + try { + $paramsString = $params.GetEnumerator() | ForEach-Object { "-$($_.Key) '$($_.Value)'" } + + $sv = Invoke-PodeWinElevatedCommand -Command 'New-Service' -Arguments ($paramsString -join ' ') -Credential $Credential + + if (!$sv) { + # Service registration failed. + throw ($PodeLocale.serviceRegistrationException -f "$Name") + } + } + catch { + $_ | Write-PodeErrorLog + throw $_ # Rethrow the error after logging + } + + return $true +} + + + + + +function Test-PodeUserServiceCreationPrivilege { + # Get the list of user privileges + $privileges = whoami /priv | Where-Object { $_ -match 'SeCreateServicePrivilege' } + + if ($privileges) { + return $true + } + else { + return $false + } +} + +<# +.SYNOPSIS + Confirms if the current user has the necessary privileges to run the script. + +.DESCRIPTION + This function checks if the user has administrative privileges on Windows or root/sudo privileges on Linux/macOS. + If the user does not have the required privileges, the script will output an appropriate message and exit. + +.PARAMETER None + This function does not accept any parameters. + +.EXAMPLE + Confirm-PodeAdminPrivilege + + This will check if the user has the necessary privileges to run the script. If not, it will output an error message and exit. + +.OUTPUTS + Exits the script if the necessary privileges are not available. + +.NOTES + This function works across Windows, Linux, and macOS, and checks for either administrative/root/sudo privileges or specific service-related permissions. +#> + +function Confirm-PodeAdminPrivilege { + # Check for administrative privileges + if (! (Test-PodeAdminPrivilege -Elevate)) { + if (Test-PodeIsWindows -and (Test-PodeUserServiceCreationPrivilege)) { + Write-PodeHost "Insufficient privileges. This script requires Administrator access or the 'SERVICE_CHANGE_CONFIG' (SeCreateServicePrivilege) permission to continue." -ForegroundColor Red + exit + } + + # Message for non-Windows (Linux/macOS) + Write-PodeHost "Insufficient privileges. This script must be run as root or with 'sudo' permissions to continue." -ForegroundColor Red + exit + } +} + +<# +.SYNOPSIS + Tests if a Linux service is registered. + +.DESCRIPTION + Checks if a specified Linux service is registered by using the `systemctl status` command. + It returns `$true` if the service is found or its status code matches either `0` or `3`. + +.PARAMETER Name + The name of the Linux service to test. + +.OUTPUTS + [bool] + Returns `$true` if the service is registered; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeLinuxServiceIsRegistered { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $nameService = Get-PodeRealServiceName -Name $Name + $systemctlStatus = systemctl status $nameService 2>&1 + $isRegistered = ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) + Write-Verbose -Message ($systemctlStatus -join '`n') + return $isRegistered +} + +<# +.SYNOPSIS + Tests if a Linux service is active. + +.DESCRIPTION + Checks if a specified Linux service is currently active by using the `systemctl is-active` command. + It returns `$true` if the service is active. + +.PARAMETER Name + The name of the Linux service to check. + +.OUTPUTS + [bool] + Returns `$true` if the service is active; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeLinuxServiceIsActive { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $nameService = Get-PodeRealServiceName -Name $Name + $systemctlIsActive = systemctl is-active $nameService 2>&1 + $isActive = $systemctlIsActive -eq 'active' + Write-Verbose -Message ($systemctlIsActive -join '`n') + return $isActive +} + +<# +.SYNOPSIS + Disables a Linux service. + +.DESCRIPTION + Disables a specified Linux service by using the `sudo systemctl disable` command. + It returns `$true` if the service is successfully disabled. + +.PARAMETER Name + The name of the Linux service to disable. + +.OUTPUTS + [bool] + Returns `$true` if the service is successfully disabled; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Disable-PodeLinuxService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $nameService = Get-PodeRealServiceName -Name $Name + $systemctlDisable = & sudo systemctl disable $nameService 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($systemctlDisable -join '`n') + return $success +} + +<# +.SYNOPSIS + Enables a Linux service. + +.DESCRIPTION + Enables a specified Linux service by using the `sudo systemctl enable` command. + It returns `$true` if the service is successfully enabled. + +.PARAMETER Name + The name of the Linux service to enable. + +.OUTPUTS + [bool] + Returns `$true` if the service is successfully enabled; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Enable-PodeLinuxService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $systemctlEnable = & sudo systemctl enable $Name 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($systemctlEnable -join '`n') + return $success +} + +<# +.SYNOPSIS + Stops a Linux service. + +.DESCRIPTION + Stops a specified Linux service by using the `systemctl stop` command. + It returns `$true` if the service is successfully stopped. + +.PARAMETER Name + The name of the Linux service to stop. + +.OUTPUTS + [bool] + Returns `$true` if the service is successfully stopped; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Stop-PodeLinuxService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $nameService = Get-PodeRealServiceName -Name $Name + #return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) + $serviceStopInfo = & sudo systemctl stop $nameService 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($serviceStopInfo -join "`n") + return $success +} + +<# +.SYNOPSIS + Starts a Linux service. + +.DESCRIPTION + Starts a specified Linux service by using the `systemctl start` command. + It returns `$true` if the service is successfully started. + +.PARAMETER Name + The name of the Linux service to start. + +.OUTPUTS + [bool] + Returns `$true` if the service is successfully started; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Start-PodeLinuxService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $nameService = Get-PodeRealServiceName -Name $Name + $serviceStartInfo = & sudo systemctl start $nameService 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($serviceStartInfo -join "`n") + return $success +} + +<# +.SYNOPSIS + Tests if a macOS service is registered. + +.DESCRIPTION + Checks if a specified macOS service is registered by using the `launchctl list` command. + It returns `$true` if the service is registered. + +.PARAMETER Name + The name of the macOS service to test. + +.PARAMETER Agent + Return only Agent type services. + +.OUTPUTS + [bool] + Returns `$true` if the service is registered; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeMacOsServiceIsRegistered { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Agent + ) + $nameService = Get-PodeRealServiceName -Name $Name + if ($Agent) { + $sudo = $false + } + else { + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) + } + + if ($sudo) { + $systemctlStatus = & sudo launchctl list $nameService 2>&1 + } + else { + $systemctlStatus = & launchctl list $nameService 2>&1 + } + $isRegistered = ($LASTEXITCODE -eq 0) + Write-Verbose -Message ($systemctlStatus -join '`n') + return $isRegistered +} + +<# +.SYNOPSIS + Checks if a Pode service is registered on the current operating system. + +.DESCRIPTION + This function determines if a Pode service with the specified name is registered, + based on the operating system. It delegates the check to the appropriate + platform-specific function or logic. + +.PARAMETER Name + The name of the Pode service to check. + +.EXAMPLE + Test-PodeServiceIsRegistered -Name 'MyService' + + Checks if the Pode service named 'MyService' is registered. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeServiceIsRegistered { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + if (Test-PodeIsWindows) { + $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" + return $null -eq $service + } + if ($IsLinux) { + return Test-PodeLinuxServiceIsRegistered -Name $Name + } + if ($IsMacOS) { + return Test-PodeMacOsServiceIsRegistered -Name $Name + } +} + +<# +.SYNOPSIS + Checks if a Pode service is active and running on the current operating system. + +.DESCRIPTION + This function determines if a Pode service with the specified name is active (running), + based on the operating system. It delegates the check to the appropriate platform-specific + function or logic. + +.PARAMETER Name + The name of the Pode service to check. + +.EXAMPLE + Test-PodeServiceIsActive -Name 'MyService' + + Checks if the Pode service named 'MyService' is active and running. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeServiceIsActive { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + if (Test-PodeIsWindows) { + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is already running + return ($service.Status -ne 'Running') + } + return $false + } + if ($IsLinux) { + return Test-PodeLinuxServiceIsActive -Name $Name + } + if ($IsMacOS) { + return Test-PodeMacOsServiceIsActive -Name $Name + } + +} + + +<# +.SYNOPSIS + Tests if a macOS service is active. + +.DESCRIPTION + Checks if a specified macOS service is currently active by looking for the "PID" value in the output of `launchctl list`. + It returns `$true` if the service is active (i.e., if a PID is found). + +.PARAMETER Name + The name of the macOS service to check. + +.OUTPUTS + [bool] + Returns `$true` if the service is active; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Test-PodeMacOsServiceIsActive { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $nameService = Get-PodeRealServiceName -Name $Name + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) + if ($sudo) { + $serviceInfo = & sudo launchctl list $nameService + } + else { + $serviceInfo = & launchctl list $nameService + } + $isActive = $serviceInfo -match '"PID" = (\d+);' + Write-Verbose -Message ($serviceInfo -join "`n") + return $isActive.Count -eq 1 +} + +<# +.SYNOPSIS + Retrieves the PID of a macOS service. + +.DESCRIPTION + Retrieves the process ID (PID) of a specified macOS service by using `launchctl list`. + If the service is not active or a PID cannot be found, the function returns `0`. + +PARAMETER Name + The name of the macOS service whose PID you want to retrieve. + +.OUTPUTS + [int] + Returns the PID of the service if it is active; otherwise, returns `0`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeMacOsServicePid { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + $nameService = Get-PodeRealServiceName -Name $Name + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) + if ($sudo) { + $serviceInfo = & sudo launchctl list $nameService + } + else { + $serviceInfo = & launchctl list $nameService + } + $pidString = $serviceInfo -match '"PID" = (\d+);' + Write-Verbose -Message ($serviceInfo -join "`n") + return $(if ($pidString.Count -eq 1) { ($pidString[0].split('= '))[1].trim(';') } else { 0 }) +} + +<# +.SYNOPSIS + Disables a macOS service. + +.DESCRIPTION + Disables a specified macOS service by using `launchctl unload` to unload the service's plist file. + It returns `$true` if the service is successfully disabled. + +.PARAMETER Name + The name of the macOS service to disable. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.OUTPUTS + [bool] + Returns `$true` if the service is successfully disabled; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Disable-PodeMacOsService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Agent + ) + # Standardize service naming for Linux/macOS + $nameService = Get-PodeRealServiceName -Name $Name + + if ($Agent) { + $sudo = $false + } + else { + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) + } + + if ($sudo) { + $systemctlDisable = & sudo launchctl unload "/Library/LaunchDaemons/$nameService.plist" 2>&1 + } + else { + $systemctlDisable = & launchctl unload "$HOME/Library/LaunchAgents/$nameService.plist" 2>&1 + } + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($systemctlDisable -join '`n') + return $success +} + +<# +.SYNOPSIS + Stops a macOS service. + +.DESCRIPTION + Stops a specified macOS service by using the `launchctl stop` command. + It returns `$true` if the service is successfully stopped. + +.PARAMETER Name + The name of the macOS service to stop. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.OUTPUTS + [bool] + Returns `$true` if the service is successfully stopped; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Stop-PodeMacOsService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Agent + ) + + return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM -Agent:$Agent) +} + +<# +.SYNOPSIS + Starts a macOS service. + +.DESCRIPTION + Starts a specified macOS service by using the `launchctl start` command. + It returns `$true` if the service is successfully started. + +.PARAMETER Name + The name of the macOS service to start. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. + +.OUTPUTS + [bool] + Returns `$true` if the service is successfully started; otherwise, `$false`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Start-PodeMacOsService { + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Agent + ) + $nameService = Get-PodeRealServiceName -Name $Name + + if ($Agent) { + $sudo = $false + } + else { + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) + } + + if ($sudo) { + $serviceStartInfo = & sudo launchctl start $nameService 2>&1 + } + else { + $serviceStartInfo = & launchctl start $nameService 2>&1 + } + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($serviceStartInfo -join "`n") + return $success +} + +<# +.SYNOPSIS + Sends a specified signal to a Pode service on Linux or macOS. + +.DESCRIPTION + The `Send-PodeServiceSignal` function sends a Unix signal (`SIGTSTP`, `SIGCONT`, `SIGHUP`, or `SIGTERM`) to a specified Pode service. It checks if the service is registered and active before sending the signal. The function supports both standard and elevated privilege operations based on the service's configuration. + +.PARAMETER Name + The name of the Pode service to signal. + +.PARAMETER Signal + The Unix signal to send to the service. Supported signals are: + - `SIGTSTP`: Stop the service temporarily (20). + - `SIGCONT`: Continue the service (18). + - `SIGHUP`: Restart the service (1). + - `SIGTERM`: Terminate the service gracefully (15). + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.OUTPUTS + [bool] Returns `$true` if the signal was successfully sent, otherwise `$false`. + +.EXAMPLE + Send-PodeServiceSignal -Name "MyPodeService" -Signal "SIGHUP" + + Sends the `SIGHUP` signal to the Pode service named "MyPodeService", instructing it to restart. + +.EXAMPLE + Send-PodeServiceSignal -Name "AnotherService" -Signal "SIGTERM" + + Sends the `SIGTERM` signal to gracefully stop the Pode service named "AnotherService". + +.NOTES + - This function is intended for use on Linux and macOS only. + - Requires administrative/root privileges to send signals to services running with elevated privileges. + - Logs verbose output for troubleshooting. + - This is an internal function and may change in future releases of Pode. +#> +function Send-PodeServiceSignal { + [CmdletBinding()] + [OutputType([bool])] + param( + # The name of the Pode service to signal + [Parameter(Mandatory = $true)] + [string] + $Name, + + # The Unix signal to send to the service + [Parameter(Mandatory = $true)] + [ValidateSet('SIGTSTP', 'SIGCONT', 'SIGHUP', 'SIGTERM')] + [string] + $Signal, + + [switch] + $Agent + ) + + # Standardize service naming for Linux/macOS + $nameService = Get-PodeRealServiceName -Name $Name + + # Map signal names to their corresponding Unix signal numbers + $signalMap = @{ + 'SIGTSTP' = 20 # Stop the process + 'SIGCONT' = 18 # Resume the process + 'SIGHUP' = 1 # Restart the process + 'SIGTERM' = 15 # Gracefully terminate the process + } + + # Retrieve the signal number from the map + $level = $signalMap[$Signal] + + # Check if the service is registered + if ((Test-PodeServiceIsRegistered -Name $nameService)) { + # Check if the service is currently active + if ((Test-PodeServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' is active. Sending $Signal signal." + + # Retrieve service details, including the PID and privilege requirement + $svc = Get-PodeService -Name $Name -Agent:$Agent + + # Send the signal based on the privilege level + if ($svc.Sudo) { + & sudo /bin/kill -$($level) $svc.Pid + } + else { + & /bin/kill -$($level) $svc.Pid + } + + # Check the exit code to determine if the signal was sent successfully + $success = $LASTEXITCODE -eq 0 + if ($success) { + Write-Verbose -Message "$Signal signal sent to service '$Name'." + } + return $success + } + else { + Write-Verbose -Message "Service '$Name' is not running." + } + } + else { + # Throw an exception if the service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + # Return false if the signal could not be sent + return $false +} + +<# +.SYNOPSIS + Waits for a Pode service to reach a specified status within a defined timeout period. + +.DESCRIPTION + The `Wait-PodeServiceStatus` function continuously checks the status of a specified Pode service and waits for it to reach the desired status (`Running`, `Stopped`, or `Suspended`). If the service does not reach the desired status within the timeout period, the function returns `$false`. + +.PARAMETER Name + The name of the Pode service to monitor. + +.PARAMETER Status + The desired status to wait for. Valid values are: + - `Running` + - `Stopped` + - `Suspended` + +.PARAMETER Timeout + The maximum time, in seconds, to wait for the service to reach the desired status. Defaults to 10 seconds. + +.EXAMPLE + Wait-PodeServiceStatus -Name "MyPodeService" -Status "Running" -Timeout 15 + + Waits up to 15 seconds for the Pode service named "MyPodeService" to reach the `Running` status. + +.EXAMPLE + Wait-PodeServiceStatus -Name "AnotherService" -Status "Stopped" + + Waits up to 10 seconds (default timeout) for the Pode service named "AnotherService" to reach the `Stopped` status. + +.OUTPUTS + [bool] Returns `$true` if the service reaches the desired status within the timeout period, otherwise `$false`. + +.NOTES + - The function checks the service status every second until the desired status is reached or the timeout period expires. + - If the service does not reach the desired status within the timeout period, the function returns `$false`. + - This is an internal function and may change in future releases of Pode. +#> +function Wait-PodeServiceStatus { + [CmdletBinding()] + [OutputType([bool])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('Running', 'Stopped', 'Suspended')] + [string] + $Status, + + [Parameter(Mandatory = $false)] + [int] + $Timeout = 10 + ) + + # Record the start time for timeout tracking + $startTime = Get-Date + Write-Verbose "Waiting for service '$Name' to reach status '$Status' with a timeout of $Timeout seconds." + + # Begin an infinite loop to monitor the service status + while ($true) { + # Retrieve the current status of the specified Pode service + $currentStatus = Get-PodeServiceStatus -Name $Name + + # Check if the service has reached the desired status + if ($currentStatus.Status -eq $Status) { + Write-Verbose "Service '$Name' has reached the desired status '$Status'." + return $true + } + + # Check if the timeout period has been exceeded + if ((Get-Date) -gt $startTime.AddSeconds($Timeout)) { + Write-Verbose "Timeout reached. Service '$Name' did not reach the desired status '$Status'." + return $false + } + + # Pause execution for 1 second before checking again + Start-Sleep -Seconds 1 + } +} + +<# +.SYNOPSIS + Retrieves the status of a Pode service on Windows, Linux, and macOS. + +.DESCRIPTION + The `Get-PodeServiceStatus` function provides detailed information about the status of a Pode service. + It queries the service's current state, process ID (PID), and whether elevated privileges (Sudo) are required, + adapting its behavior to the platform it runs on: + + - **Windows**: Retrieves service information using the `Win32_Service` class and maps common states to Pode-specific ones. + - **Linux**: Uses `systemctl` to determine the service status and reads additional state information from custom Pode state files if available. + - **macOS**: Checks service status via `launchctl` and processes custom Pode state files when applicable. + +.PARAMETER Name + Specifies the name of the Pode service to query. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.EXAMPLE + Get-PodeServiceStatus -Name "MyPodeService" + Retrieves the status of the Pode service named "MyPodeService". + +.EXAMPLE + Get-PodeServiceStatus -Name "MyPodeService" -Agent + Retrieves the status of the agent-type Pode service named "MyPodeService" (macOS only). + +.OUTPUTS + [PSCustomObject] The function returns a custom object with the following properties: + - **Name**: The name of the service. + - **Status**: The current status of the service (e.g., Running, Stopped, Suspended). + - **Pid**: The process ID of the service. + - **Sudo**: A boolean indicating whether elevated privileges are required. + - **PathName**: The path to the service's configuration or executable. + - **Type**: The type of the service (e.g., Service, Daemon, Agent). + +.NOTES + - **Supported Status States**: Running, Stopped, Suspended, Starting, Stopping, Pausing, Resuming, Unknown. + - Requires administrative/root privileges for accessing service information on Linux and macOS. + - **Platform-specific Behaviors**: + - **Windows**: Leverages CIM to query service information and map states. + - **Linux**: Relies on `systemctl` and custom Pode state files for service details. + - **macOS**: Uses `launchctl` and Pode state files to assess service status. + - If the specified service is not found, the function returns `$null`. + - Logs errors and warnings to assist in troubleshooting. + - This function is internal to Pode and subject to changes in future releases. +#> +function Get-PodeServiceStatus { + [CmdletBinding()] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Agent + ) + + + if (Test-PodeIsWindows) { + # Check if the service exists on Windows + $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" + + if ($service) { + switch ($service.State) { + 'Running' { $status = 'Running' } + 'Stopped' { $status = 'Stopped' } + 'Paused' { $status = 'Suspended' } + 'StartPending' { $status = 'Starting' } + 'StopPending' { $status = 'Stopping' } + 'PausePending' { $status = 'Pausing' } + 'ContinuePending' { $status = 'Resuming' } + default { $status = 'Unknown' } + } + return [PSCustomObject]@{ + PsTypeName = 'PodeService' + Name = $Name + Status = $status + Pid = $service.ProcessId + Sudo = $true + PathName = $service.PathName + Type = 'Service' + } + + } + else { + Write-Verbose -Message "Service '$Name' not found." + return $null + } + } + + elseif ($IsLinux) { + try { + $nameService = Get-PodeRealServiceName -Name $Name + # Check if the service exists on Linux (systemd) + if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { + $servicePid = 0 + $status = $(systemctl show -p ActiveState $nameService | awk -F'=' '{print $2}') + + switch ($status) { + 'active' { + $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') + $stateFilePath = "/var/run/podemonitor/$servicePid.state" + if (Test-Path -Path $stateFilePath) { + $status = Get-Content -Path $stateFilePath -Raw + $status = $status.Substring(0, 1).ToUpper() + $status.Substring(1) + } + } + 'reloading' { + $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') + $status = 'Running' + } + 'maintenance' { + $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') + $status = 'Suspended' + } + 'inactive' { + $status = 'Stopped' + } + 'failed' { + $status = 'Stopped' + } + 'activating' { + $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') + $status = 'Starting' + } + 'deactivating' { + $status = 'Stopping' + } + default { + $status = 'Stopped' + } + } + return [PSCustomObject]@{ + PsTypeName = 'PodeService' + Name = $Name + Status = $status + Pid = $servicePid + Sudo = $true + PathName = "/etc/systemd/system/$nameService" + Type = 'Service' + } + } + else { + Write-Verbose -Message "Service '$nameService' not found." + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $null + } + } + + elseif ($IsMacOS) { + try { + $nameService = Get-PodeRealServiceName -Name $Name + # Check if the service exists on macOS (launchctl) + if ((Test-PodeMacOsServiceIsRegistered $nameService -Agent:$Agent)) { + $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match + + if ($Agent) { + $sudo = $false + } + else { + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) + } + + if ($sudo) { + $stateFilePath = "/Library/LaunchDaemons/PodeMonitor/$servicePid.state" + $plistPath = "/Library/LaunchDaemons/$($nameService).plist" + $serviceType = 'Daemon' + } + else { + $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" + $plistPath = "$($HOME)/Library/LaunchAgents/$($nameService).plist" + $serviceType = 'Agent' + } + + if (Test-Path -Path $stateFilePath) { + $status = Get-Content -Path $stateFilePath -Raw + $status = $status.Substring(0, 1).ToUpper() + $status.Substring(1) + } + else { + $status = 'Stopped' + } + + return [PSCustomObject]@{ + PsTypeName = 'PodeService' + Name = $Name + Status = $status + Pid = $servicePid + Sudo = $sudo + PathName = $plistPath + Type = $serviceType + } + } + else { + Write-Verbose -Message "Service '$Name' not found." + return $null + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $null + } + } + +} + +<# +.SYNOPSIS + Returns the standardized service name for a Pode service based on the current platform. + +.DESCRIPTION + The `Get-PodeRealServiceName` function formats a Pode service name to match platform-specific conventions: + - On macOS, the service name is prefixed with `pode.` and suffixed with `.service`, with spaces replaced by underscores. + - On Linux, the service name is suffixed with `.service`, with spaces replaced by underscores. + - On Windows, the service name is returned as provided. + +.PARAMETER Name + The name of the Pode service to standardize. + +.EXAMPLE + Get-PodeRealServiceName -Name "My Pode Service" + + For macOS, returns: `pode.My_Pode_Service.service`. + For Linux, returns: `My_Pode_Service.service`. + For Windows, returns: `My Pode Service`. + +.NOTES + This is an internal function and may change in future releases of Pode. +#> +function Get-PodeRealServiceName { + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # If the name already ends with '.service', return it directly + if ($Name -like '*.service') { + return $Name + } + + # Standardize service naming based on platform + if ($IsMacOS) { + return "pode.$Name.service".Replace(' ', '_') + } + elseif ($IsLinux) { + return "$Name.service".Replace(' ', '_') + } + else { + # Assume Windows or unknown platform + return $Name + } +} diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 5d7054bd7..dad3edb74 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -222,7 +222,9 @@ function Start-PodeServer { end { if ($pipelineItemCount -gt 1) { throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } # Store the name of the current runspace + } + + # Store the name of the current runspace $previousRunspaceName = Get-PodeCurrentRunspaceName # Sets the name of the current runspace Set-PodeCurrentRunspaceName -Name 'PodeServer' @@ -231,6 +233,33 @@ function Start-PodeServer { $Script:PodeContext = $null $ShowDoneMessage = $true + # check if podeWatchdog is configured + if ($PodeService) { + if ($null -ne $PodeService.DisableTermination -or + $null -ne $PodeService.Quiet -or + $null -ne $PodeService.PipeName -or + $null -ne $PodeService.DisableConsoleInput + ) { + $DisableTermination = [switch]$PodeService.DisableTermination + $Quiet = [switch]$PodeService.Quiet + $DisableConsoleInput = [switch]$PodeService.DisableConsoleInput + $IgnoreServerConfig = [switch]$PodeService.IgnoreServerConfig + + if (!([string]::IsNullOrEmpty($PodeService.ConfigFile)) -and !$PodeService.IgnoreServerConfig) { + $ConfigFile = $PodeService.ConfigFile + } + + $monitorService = @{ + DisableTermination = $PodeService.DisableTermination + Quiet = $PodeService.Quiet + PipeName = $PodeService.PipeName + DisableConsoleInput = $PodeService.DisableConsoleInput + ConfigFile = $PodeService.ConfigFile + IgnoreServerConfig = $PodeService.IgnoreServerConfig + } + Write-PodeHost $PodeService -Explode -Force } + } + try { # if we have a filepath, resolve it - and extract a root path from it if ($PSCmdlet.ParameterSetName -ieq 'file') { @@ -268,6 +297,7 @@ function Start-PodeServer { EnableBreakpoints = $EnableBreakpoints IgnoreServerConfig = $IgnoreServerConfig ConfigFile = $ConfigFile + Service = $monitorService } diff --git a/src/Public/Endpoint.ps1 b/src/Public/Endpoint.ps1 index d2d9fa523..57eccd90d 100644 --- a/src/Public/Endpoint.ps1 +++ b/src/Public/Endpoint.ps1 @@ -392,7 +392,7 @@ function Add-PodeEndpoint { $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)) { + if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and !(Test-PodeAdminPrivilege -Console)) { # Must be running with administrator privileges to listen on non-localhost addresses throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) } diff --git a/src/Public/Metrics.ps1 b/src/Public/Metrics.ps1 index 2334f27c5..62819f161 100644 --- a/src/Public/Metrics.ps1 +++ b/src/Public/Metrics.ps1 @@ -25,6 +25,18 @@ .PARAMETER ExcludeMilliseconds Omits milliseconds from the human-readable output when `-Format` is not `Milliseconds`. +.PARAMETER Readable + If supplied, the uptime will be returned in a human-readable format instead of milliseconds. + +.PARAMETER OutputType + Specifies the format for the human-readable output. Valid options are: + - 'Verbose' for detailed descriptions (e.g., "1 day, 2 hours, 3 minutes"). + - 'Compact' for a compact format (e.g., "dd:hh:mm:ss"). + - Default is concise format (e.g., "1d 2h 3m"). + +.PARAMETER ExcludeMilliseconds + If supplied, milliseconds will be excluded from the human-readable output. + .EXAMPLE $currentUptime = Get-PodeServerUptime # Output: 123456789 (milliseconds) @@ -56,6 +68,7 @@ function Get-PodeServerUptime { [CmdletBinding()] [OutputType([long], [string])] param( + # Common to all parameter sets [switch] $Total, @@ -63,7 +76,7 @@ function Get-PodeServerUptime { [ValidateSet('Milliseconds', 'Concise', 'Compact', 'Verbose')] [string] $Format = 'Milliseconds', - + [switch] $ExcludeMilliseconds ) diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 new file mode 100644 index 000000000..a96831b6b --- /dev/null +++ b/src/Public/Service.ps1 @@ -0,0 +1,1184 @@ +<# +.SYNOPSIS + Registers a new Pode-based PowerShell worker as a service on Windows, Linux, or macOS. + +.DESCRIPTION + The `Register-PodeService` function configures and registers a Pode-based service that runs a PowerShell worker across multiple platforms + (Windows, Linux, macOS). It creates the service with parameters such as paths to the worker script, log files, and service-specific settings. + A `srvsettings.json` configuration file is generated, and the service can be optionally started after registration. + +.PARAMETER Name + Specifies the name of the service to be registered. This is a required parameter. + +.PARAMETER Description + A brief description of the service. Defaults to "This is a Pode service." + +.PARAMETER DisplayName + Specifies the display name for the service (Windows only). Defaults to "Pode Service($Name)". + +.PARAMETER StartupType + Specifies the startup type of the service ('Automatic' or 'Manual'). Defaults to 'Automatic'. + +.PARAMETER ParameterString + Any additional parameters to pass to the worker script when the service is run. Defaults to an empty string. + +.PARAMETER LogServicePodeHost + Enables logging for the Pode service host. If not provided, the service runs in quiet mode. + +.PARAMETER ShutdownWaitTimeMs + Maximum time in milliseconds to wait for the service to shut down gracefully before forcing termination. Defaults to 30,000 milliseconds (30 seconds). + +.PARAMETER StartMaxRetryCount + The maximum number of retries to start the PowerShell process before giving up. Default is 3 retries. + +.PARAMETER StartRetryDelayMs + The delay (in milliseconds) between retry attempts to start the PowerShell process. Default is 5,000 milliseconds (5 seconds). + +.PARAMETER WindowsUser + Specifies the username under which the service will run on Windows. Defaults to the current user if not provided. + +.PARAMETER LinuxUser + Specifies the username under which the service will run on Linux. Defaults to the current user if not provided. + +.PARAMETER Agent + Create an Agent instead of a Daemon on macOS (macOS only). + +.PARAMETER Start + A switch to start the service immediately after registration. + +.PARAMETER Password + A secure password for the service account (Windows only). If omitted, the service account defaults to 'NT AUTHORITY\SYSTEM'. + +.PARAMETER SecurityDescriptorSddl + A security descriptor in SDDL format, specifying the permissions for the service (Windows only). + +.PARAMETER SettingsPath + Specifies the directory to store the service configuration file (`_svcsettings.json`). If not provided, a default directory is used. + +.PARAMETER LogPath + Specifies the path for the service log files. If not provided, a default log directory is used. + +.PARAMETER LogLevel + Specifies the log verbosity level. Valid values are 'Debug', 'Info', 'Warn', 'Error', or 'Critical'. Defaults to 'Info'. + +.PARAMETER LogMaxFileSize + Specifies the maximum size of the log file in bytes. Defaults to 10 MB (10,485,760 bytes). + +.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`. + +.EXAMPLE + Register-PodeService -Name "PodeExampleService" -Description "Example Pode Service" -ParameterString "-Verbose" + + This example registers a Pode service named "PodeExampleService" with verbose logging enabled. + +.NOTES + - Supports cross-platform service registration on Windows, Linux, and macOS. + - Generates a `srvsettings.json` file with service-specific configurations. + - Automatically starts the service using the `-Start` switch after registration. + - Dynamically obtains the PowerShell executable path for compatibility across platforms. +#> +function Register-PodeService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $Description = 'This is a Pode service.', + + [Parameter()] + [string] + $DisplayName = "Pode Service($Name)", + + [Parameter()] + [string] + [validateset('Manual', 'Automatic')] + $StartupType = 'Automatic', + + [Parameter()] + [string] + $SecurityDescriptorSddl, + + [Parameter()] + [string] + $ParameterString = '', + + [Parameter()] + [switch] + $LogServicePodeHost, + + [Parameter()] + [int] + $ShutdownWaitTimeMs = 30000, + + [Parameter()] + [int] + $StartMaxRetryCount = 3, + + [Parameter()] + [int] + $StartRetryDelayMs = 5000, + + [Parameter()] + [string] + $WindowsUser, + + [Parameter()] + [string] + $LinuxUser, + + [Parameter()] + [switch] + $Start, + + [Parameter()] + [switch] + $Agent, + + [Parameter()] + [securestring] + $Password, + + [Parameter()] + [string] + $SettingsPath, + + [Parameter()] + [string] + $LogPath, + + [Parameter()] + [string] + [validateset('Debug', 'Info', 'Warn', 'Error', 'Critical')] + $LogLevel = 'Info', + + [Parameter()] + [Int64] + $LogMaxFileSize = 10 * 1024 * 1024, + + [Parameter()] + [string] + $ConfigFile, + + [switch] + $IgnoreServerConfig + ) + + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege + + try { + # Obtain the script path and directory + if ($MyInvocation.ScriptName) { + $ScriptPath = $MyInvocation.ScriptName + $MainScriptPath = Split-Path -Path $ScriptPath -Parent + } + else { + return $null + } + # Define log paths and ensure the log directory exists + if (! $LogPath) { + $LogPath = Join-Path -Path $MainScriptPath -ChildPath 'logs' + } + + if (! (Test-Path -Path $LogPath -PathType Container)) { + $null = New-Item -Path $LogPath -ItemType Directory -Force + } + + $LogFilePath = Join-Path -Path $LogPath -ChildPath "$($Name)_svc.log" + + # Dynamically get the PowerShell executable path + $PwshPath = (Get-Process -Id $PID).Path + + # Define configuration directory and settings file path + if (!$SettingsPath) { + $SettingsPath = Join-Path -Path $MainScriptPath -ChildPath 'svc_settings' + } + + if (! (Test-Path -Path $SettingsPath -PathType Container)) { + $null = New-Item -Path $settingsPath -ItemType Directory + } + + if (Test-PodeIsWindows) { + if ([string]::IsNullOrEmpty($WindowsUser)) { + if ( ($null -ne $Password)) { + $UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + } + } + else { + $UserName = $WindowsUser + if ( ($null -eq $Password)) { + throw ($Podelocale.passwordRequiredForServiceUserException -f $UserName) + } + } + } + elseif ($IsLinux) { + if ([string]::IsNullOrEmpty($LinuxUser)) { + $UserName = [System.Environment]::UserName + } + else { + $UserName = $LinuxUser + } + } + + $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_svcsettings.json" + Write-Verbose -Message "Service '$Name' setting : $settingsFile." + + # Generate the service settings JSON file + $jsonContent = @{ + PodeMonitorWorker = @{ + ScriptPath = $ScriptPath + PwshPath = $PwshPath + ParameterString = $ParameterString + LogFilePath = $LogFilePath + Quiet = !$LogServicePodeHost.IsPresent + DisableTermination = $true + ShutdownWaitTimeMs = $ShutdownWaitTimeMs + Name = $Name + StartMaxRetryCount = $StartMaxRetryCount + StartRetryDelayMs = $StartRetryDelayMs + LogLevel = $LogLevel.ToUpper() + LogMaxFileSize = $LogMaxFileSize + ConfigFile = $ConfigFile + IgnoreServerConfig = $IgnoreServerConfig.IsPresent + } + } + + # Save JSON to the settings file + $jsonContent | ConvertTo-Json | Set-Content -Path $settingsFile -Encoding UTF8 + + # Determine OS architecture and call platform-specific registration functions + $osArchitecture = Get-PodeOSPwshArchitecture + + if ([string]::IsNullOrEmpty($osArchitecture)) { + Write-Verbose 'Unsupported Architecture' + return $false + } + + # Get the directory path where the Pode module is installed and store it in $binPath + $binPath = Join-Path -Path ((Get-Module -Name Pode).ModuleBase) -ChildPath 'Bin' + + if (Test-PodeIsWindows) { + $param = @{ + Name = $Name + Description = $Description + DisplayName = $DisplayName + StartupType = $StartupType + BinPath = $binPath + SettingsFile = $settingsFile + Credential = if ($Password) { [pscredential]::new($UserName, $Password) }else { $null } + SecurityDescriptorSddl = $SecurityDescriptorSddl + OsArchitecture = "win-$osArchitecture" + } + $operation = Register-PodeMonitorWindowsService @param + } + elseif ($IsLinux) { + $param = @{ + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + Group = $Group + Start = $Start + OsArchitecture = "linux-$osArchitecture" + } + $operation = Register-PodeLinuxService @param + } + elseif ($IsMacOS) { + $param = @{ + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + OsArchitecture = "osx-$osArchitecture" + LogPath = $LogPath + Agent = $Agent + } + + $operation = Register-PodeMacService @param + } + + # Optionally start the service if requested + if ( $operation -and $Start.IsPresent) { + $operation = Start-PodeService -Name $Name + } + + return $operation + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } +} + +<# +.SYNOPSIS + Start a Pode-based service on Windows, Linux, or macOS. + +.DESCRIPTION + The `Start-PodeService` function ensures that a specified Pode-based service is running. If the service is not registered or fails to start, the function throws an error. It supports platform-specific service management commands: + - Windows: Uses `sc.exe`. + - Linux: Uses `systemctl`. + - macOS: Uses `launchctl`. + +.PARAMETER Name + The name of the service to start. + +.PARAMETER Async + Indicates whether to return immediately after issuing the start command. If not specified, the function waits until the service reaches the 'Running' state. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for the service to reach the 'Running' state when not using `-Async`. Defaults to 10 seconds. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.EXAMPLE + Start-PodeService -Name 'MyService' + + Starts the service named 'MyService' if it is not already running. + +.EXAMPLE + Start-PodeService -Name 'MyService' -Async + + Starts the service named 'MyService' and returns immediately. + +.NOTES + - This function checks for necessary administrative/root privileges before execution. + - Service state management behavior: + - If the service is already running, no action is taken. + - If the service is not registered, an error is thrown. + - Service name is retrieved from the `srvsettings.json` file if available. + - Platform-specific commands are invoked to manage service states: + - Windows: `sc.exe start`. + - Linux: `sudo systemctl start`. + - macOS: `sudo launchctl start`. + - Errors and logs are captured for debugging purposes. +#> +function Start-PodeService { + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [ValidateRange(1, 300)] + [int] $Timeout = 10, + + [switch] + $Agent + ) + try { + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + + # Get the service status + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent + if (!$service) { + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + Write-Verbose -Message "Service '$Name' current state: $($service.Status)." + + # Handle the current service state + switch ($service.Status) { + 'Running' { + Write-Verbose -Message "Service '$Name' is already running." + return $true + } + 'Suspended' { + Write-Verbose -Message "Service '$Name' is suspended. Cannot start a suspended service." + return $false + } + 'Stopped' { + Write-Verbose -Message "Service '$Name' is currently stopped. Attempting to start..." + } + { $_ -eq 'Starting' -or $_ -eq 'Stopping' -or $_ -eq 'Pausing' -or $_ -eq 'Resuming' } { + Write-Verbose -Message "Service '$Name' is transitioning state ($($service.Status)). Cannot start at this time." + return $false + } + default { + Write-Verbose -Message "Service '$Name' is in an unknown state ($($service.Status))." + return $false + } + } + + # Start the service based on the OS + $serviceStarted = $false + if (Test-PodeIsWindows) { + $serviceStarted = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "start '$Name'" + } + elseif ($IsLinux) { + $serviceStarted = Start-PodeLinuxService -Name $Name + } + elseif ($IsMacOS) { + $serviceStarted = Start-PodeMacOsService -Name $Name -Agent:$Agent + } + + # Check if the service start command failed + if (!$serviceStarted) { + throw ($PodeLocale.serviceCommandFailedException -f 'Start', $Name) + } + + # Handle async or wait for start + if ($Async) { + Write-Verbose -Message "Async mode: Service start command issued for '$Name'." + return $true + } + else { + Write-Verbose -Message "Waiting for service '$Name' to start (timeout: $Timeout seconds)..." + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } +} + +<# +.SYNOPSIS + Stop a Pode-based service on Windows, Linux, or macOS. + +.DESCRIPTION + The `Stop-PodeService` function ensures that a specified Pode-based service is stopped. If the service is not registered or fails to stop, the function throws an error. It supports platform-specific service management commands: + - Windows: Uses `sc.exe`. + - Linux: Uses `systemctl`. + - macOS: Uses `launchctl`. + +.PARAMETER Name + The name of the service to stop. + +.PARAMETER Async + Indicates whether to return immediately after issuing the stop command. If not specified, the function waits until the service reaches the 'Stopped' state. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for the service to reach the 'Stopped' state when not using `-Async`. Defaults to 10 seconds. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.EXAMPLE + Stop-PodeService -Name 'MyService' + + Stops the service named 'MyService' if it is currently running. + +.EXAMPLE + Stop-PodeService -Name 'MyService' -Async + + Stops the service named 'MyService' and returns immediately. + +.NOTES + - This function checks for necessary administrative/root privileges before execution. + - Service state management behavior: + - If the service is not running, no action is taken. + - If the service is not registered, an error is thrown. + - Service name is retrieved from the `srvsettings.json` file if available. + - Platform-specific commands are invoked to manage service states: + - Windows: `sc.exe`. + - Linux: `sudo systemctl stop`. + - macOS: `sudo launchctl stop`. + - Errors and logs are captured for debugging purposes. + +#> +function Stop-PodeService { + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] + $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [ValidateRange(1, 300)] + [int] + $Timeout = 10, + + [switch] + $Agent + ) + try { + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + + # Get the service status + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent + if (!$service) { + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + Write-Verbose -Message "Service '$Name' current state: $($service.Status)." + + # Handle service states + switch ($service.Status) { + 'Stopped' { + Write-Verbose -Message "Service '$Name' is already stopped." + return $true + } + { $_ -eq 'Running' -or $_ -eq 'Suspended' } { + Write-Verbose -Message "Service '$Name' is currently $($service.Status). Attempting to stop..." + } + { $_ -eq 'Starting' -or $_ -eq 'Stopping' -or $_ -eq 'Pausing' -or $_ -eq 'Resuming' } { + Write-Verbose -Message "Service '$Name' is transitioning state ($($service.Status)). Cannot stop at this time." + return $false + } + default { + Write-Verbose -Message "Service '$Name' is in an unknown state ($($service.Status))." + return $false + } + } + + # Stop the service + $serviceStopped = $false + if (Test-PodeIsWindows) { + $serviceStopped = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "stop '$Name'" + } + elseif ($IsLinux) { + $serviceStopped = Stop-PodeLinuxService -Name $Name + } + elseif ($IsMacOS) { + $serviceStopped = Stop-PodeMacOsService -Name $Name -Agent:$Agent + } + + if (!$serviceStopped) { + throw ($PodeLocale.serviceCommandFailedException -f 'Stop', $Name) + } + + # Handle async or wait for stop + if ($Async) { + Write-Verbose -Message "Async mode: Service stop command issued for '$Name'." + return $true + } + else { + Write-Verbose -Message "Waiting for service '$Name' to stop (timeout: $Timeout seconds)..." + return Wait-PodeServiceStatus -Name $Name -Status Stopped -Timeout $Timeout + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } +} + + +<# +.SYNOPSIS + Suspend a specified service on Windows systems. + +.DESCRIPTION + The `Suspend-PodeService` function attempts to suspend a specified service by name. This functionality is supported only on Windows systems using `sc.exe`. On Linux and macOS, the suspend functionality for services is not natively available, and an appropriate error message is returned. + +.PARAMETER Name + The name of the service to suspend. + +.PARAMETER Async + Indicates whether to return immediately after issuing the suspend command. If not specified, the function waits until the service reaches the 'Suspended' state. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for the service to reach the 'Suspended' state when not using `-Async`. Defaults to 10 seconds. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.EXAMPLE + Suspend-PodeService -Name 'MyService' + + Suspends the service named 'MyService' if it is currently running. + +.NOTES + - This function requires administrative/root privileges to execute. + - Platform-specific behavior: + - Windows: Uses `sc.exe` with the `pause` argument. + - Linux: Sends the `SIGTSTP` signal to the service process. + - macOS: Sends the `SIGTSTP` signal to the service process. + - On Linux and macOS, an error is logged if the signal command fails or the functionality is unavailable. + - If the service is already suspended, no action is taken. + - If the service is not registered, an error is thrown. + +#> +function Suspend-PodeService { + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [ValidateRange(1, 300)] + [int] $Timeout = 10, + + [switch] + $Agent + ) + try { + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + + # Get the service status + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent + if (!$service) { + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + Write-Verbose -Message "Service '$Name' current state: $($service.Status)." + + # Handle the current service state + switch ($service.Status) { + 'Suspended' { + Write-Verbose -Message "Service '$Name' is already suspended." + return $true + } + 'Running' { + Write-Verbose -Message "Service '$Name' is currently running. Attempting to suspend..." + } + 'Stopped' { + Write-Verbose -Message "Service '$Name' is stopped. Cannot suspend a stopped service." + return $false + } + { $_ -eq 'Starting' -or $_ -eq 'Stopping' -or $_ -eq 'Pausing' -or $_ -eq 'Resuming' } { + Write-Verbose -Message "Service '$Name' is transitioning state ($($service.Status)). Cannot suspend at this time." + return $false + } + default { + Write-Verbose -Message "Service '$Name' is in an unknown state ($($service.Status))." + return $false + } + } + + # Suspend the service based on the OS + $serviceSuspended = $false + if (Test-PodeIsWindows) { + $serviceSuspended = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "pause '$Name'" + } + elseif ($IsLinux ) { + $serviceSuspended = ( Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP') + } + elseif ( $IsMacOS) { + $serviceSuspended = ( Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP' -Agent:$Agent) + } + + # Check if the service suspend command failed + if (!$serviceSuspended) { + throw ($PodeLocale.serviceCommandFailedException -f 'Suspend', $Name) + } + + # Handle async or wait for suspend + if ($Async) { + Write-Verbose -Message "Async mode: Service suspend command issued for '$Name'." + return $true + } + else { + Write-Verbose -Message "Waiting for service '$Name' to suspend (timeout: $Timeout seconds)..." + return Wait-PodeServiceStatus -Name $Name -Status Suspended -Timeout $Timeout + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } +} + + +<# +.SYNOPSIS + Resume a specified service on Windows systems. + +.DESCRIPTION + The `Resume-PodeService` function attempts to resume a specified service by name. This functionality is supported only on Windows systems using `sc.exe`. On Linux and macOS, the resume functionality for services is not natively available, and an appropriate error message is returned. + +.PARAMETER Name + The name of the service to resume. + +.PARAMETER Async + Indicates whether to return immediately after issuing the resume command. If not specified, the function waits until the service reaches the 'Running' state. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for the service to reach the 'Running' state when not using `-Async`. Defaults to 10 seconds. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.EXAMPLE + Resume-PodeService -Name 'MyService' + + Resumes the service named 'MyService' if it is currently paused. + +.NOTES + - This function requires administrative/root privileges to execute. + - Platform-specific behavior: + - Windows: Uses `sc.exe` with the `continue` argument. + - Linux: Sends the `SIGCONT` signal to the service process. + - macOS: Sends the `SIGCONT` signal to the service process. + - On Linux and macOS, an error is logged if the signal command fails or the functionality is unavailable. + - If the service is not paused, no action is taken. + - If the service is not registered, an error is thrown. + +#> +function Resume-PodeService { + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [ValidateRange(1, 300)] + [int] $Timeout = 10, + + [switch] + $Agent + ) + try { + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + + # Get the service status + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent + if (!$service) { + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + Write-Verbose -Message "Service '$Name' current state: $($service.Status)." + + # Handle the current service state + switch ($service.Status) { + 'Running' { + Write-Verbose -Message "Service '$Name' is already running. No need to resume." + return $true + } + 'Suspended' { + Write-Verbose -Message "Service '$Name' is currently suspended. Attempting to resume..." + } + 'Stopped' { + Write-Verbose -Message "Service '$Name' is stopped. Cannot resume a stopped service." + return $false + } + { $_ -eq 'Starting' -or $_ -eq 'Stopping' -or $_ -eq 'Pausing' -or $_ -eq 'Resuming' } { + Write-Verbose -Message "Service '$Name' is transitioning state ($($service.Status)). Cannot resume at this time." + return $false + } + default { + Write-Verbose -Message "Service '$Name' is in an unknown state ($($service.Status))." + return $false + } + } + + # Resume the service based on the OS + $serviceResumed = $false + if (Test-PodeIsWindows) { + $serviceResumed = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "continue '$Name'" + } + elseif ($IsLinux) { + $serviceResumed = Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' + } + elseif ($IsMacOS) { + $serviceResumed = Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' -Agent:$Agent + } + + # Check if the service resume command failed + if (!$serviceResumed) { + throw ($PodeLocale.serviceCommandFailedException -f 'Resume', $Name) + } + + # Handle async or wait for resume + if ($Async) { + Write-Verbose -Message "Async mode: Service resume command issued for '$Name'." + return $true + } + else { + Write-Verbose -Message "Waiting for service '$Name' to resume (timeout: $Timeout seconds)..." + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } +} + +<# +.SYNOPSIS + Unregisters a Pode-based service across different platforms (Windows, Linux, and macOS). + +.DESCRIPTION + The `Unregister-PodeService` function removes a Pode-based service by checking its status and unregistering it from the system. + The function can stop the service forcefully if it is running, and then remove the service from the service manager. + It works on Windows, Linux (systemd), and macOS (launchctl). + +.PARAMETER Force + A switch parameter that forces the service to stop before unregistering. If the service is running and this parameter is not specified, + the function will throw an error. + +.PARAMETER Name + The name of the service. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.EXAMPLE + Unregister-PodeService -Force + + Unregisters the Pode-based service, forcefully stopping it if it is currently running. + +.EXAMPLE + Unregister-PodeService + + Unregisters the Pode-based service if it is not running. If the service is running, the function throws an error unless the `-Force` parameter is used. + +.NOTES + - The function retrieves the service name from the `srvsettings.json` file located in the script directory. + - On Windows, it uses `Get-Service`, `Stop-Service`, and `Remove-Service`. + - On Linux, it uses `systemctl` to stop, disable, and remove the service. + - On macOS, it uses `launchctl` to stop and unload the service. +#> +function Unregister-PodeService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [switch] + $Force, + + [switch] + $Agent + ) + + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + + # Get the service status + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent + if (!$service) { + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + Write-Verbose -Message "Service '$Name' current state: $($service.Status)." + + # Handle service state + if ($service.Status -ne 'Stopped') { + if ($Force) { + Write-Verbose -Message "Service '$Name' is not stopped. Stopping the service due to -Force parameter." + if (!(Stop-PodeService -Name $Name)) { + Write-Verbose -Message "Service '$Name' is not stopped." + return $false + } + Write-Verbose -Message "Service '$Name' has been stopped." + } + else { + Write-Verbose -Message "Service '$Name' is not stopped. Use -Force to stop and unregister it." + return $false + } + } + + if (Test-PodeIsWindows) { + + # Remove the service + $null = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "delete '$Name'" + if (Get-PodeService -Name $Name -ErrorAction SilentlyContinue) { + Write-Verbose -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) + } + + Write-Verbose -Message "Service '$Name' unregistered successfully." + + # Remove the service configuration + if ($service.PathName -match '"([^"]+)" "([^"]+)"') { + $argument = $Matches[2] + if ( (Test-Path -Path ($argument) -PathType Leaf)) { + Remove-Item -Path ($argument) -ErrorAction SilentlyContinue + } + } + return $true + + } + + elseif ($IsLinux) { + if (! (Disable-PodeLinuxService -Name $Name)) { + Write-Verbose -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) + } + + Write-Verbose -Message "Service '$Name' unregistered successfully." + + # Read the content of the service file + if ((Test-path -path $service.PathName -PathType Leaf)) { + $serviceFileContent = & sudo cat $service.PathName + # Extract the SettingsFile from the ExecStart line using regex + $execStart = ($serviceFileContent | Select-String -Pattern 'ExecStart=.*\s+(.*)').ToString() + # Find the index of '/PodeMonitor ' in the string + $index = $execStart.IndexOf('/PodeMonitor ') + ('/PodeMonitor '.Length) + # Extract everything after '/PodeMonitor ' + $settingsFile = $execStart.Substring($index).trim('"') + + & sudo rm $settingsFile + Write-Verbose -Message "Settings file '$settingsFile' removed." + + & sudo rm $service.PathName + Write-Verbose -Message "Service file '$($service.PathName)' removed." + } + + # Reload systemd to apply changes + & sudo systemctl daemon-reload + Write-Verbose -Message 'Systemd daemon reloaded.' + return $true + } + + elseif ($IsMacOS) { + # Disable and unregister the service + if (!(Disable-PodeMacOsService -Name $Name -Agent:$Agent)) { + Write-Verbose -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) + } + + Write-Verbose -Message "Service '$Name' unregistered successfully." + + # Check if the plist file exists + if (Test-Path -Path $service.PathName) { + # Read the content of the plist file + $plistXml = [xml](Get-Content -Path $service.PathName -Raw) + if ($plistXml.plist.dict.array.string.Count -ge 2) { + # Extract the second string in the ProgramArguments array (the settings file path) + $settingsFile = $plistXml.plist.dict.array.string[1] + if ($service.Sudo) { + & sudo rm $settingsFile + Write-Verbose -Message "Settings file '$settingsFile' removed." + + & sudo rm $service.PathName + Write-Verbose -Message "Service file '$($service.PathName)' removed." + } + else { + Remove-Item -Path $settingsFile -ErrorAction SilentlyContinue + Write-Verbose -Message "Settings file '$settingsFile' removed." + + Remove-Item -Path $service.PathName -ErrorAction SilentlyContinue + Write-Verbose -Message "Service file '$($service.PathName)' removed." + } + } + } + return $true + } +} + + +<# +.SYNOPSIS + Retrieves the status of a Pode service across different platforms (Windows, Linux, and macOS). + +.DESCRIPTION + The `Get-PodeService` function checks if a Pode-based service is running or stopped on the host system. + It supports Windows (using `Get-Service`), Linux (using `systemctl`), and macOS (using `launchctl`). + The function returns a consistent result across all platforms by providing the service name and status in + a hashtable format. The status is mapped to common states like "Running," "Stopped," "Starting," and "Stopping." + +.PARAMETER Name + The name of the service. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.OUTPUTS + Hashtable + The function returns a hashtable containing the service name and its status. + For example: @{ Name = "MyService"; Status = "Running" } + +.EXAMPLE + Get-PodeService + + Retrieves the current status of the Pode service defined in the `srvsettings.json` configuration file. + +.EXAMPLE + Get-PodeService + + On Windows: + @{ Name = "MyService"; Status = "Running" } + + On Linux: + @{ Name = "MyService"; Status = "Stopped" } + + On macOS: + @{ Name = "MyService"; Status = "Unknown" } + +.NOTES + - The function reads the service name from the `srvsettings.json` file in the script's directory. + - For Windows, it uses the `Get-Service` cmdlet. + - For Linux, it uses `systemctl` to retrieve the service status. + - For macOS, it uses `launchctl` to check if the service is running. +#> +function Get-PodeService { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Agent + ) + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege + return Get-PodeServiceStatus -Name $Name -Agent:$Agent +} + +<# +.SYNOPSIS + Restart a Pode service on Windows, Linux, or macOS by sending the appropriate restart signal. + +.DESCRIPTION + The `Restart-PodeService` function handles the restart operation for a Pode service across multiple platforms: + - Windows: Sends a restart control signal (128) using `sc.exe control`. + - Linux/macOS: Sends the `SIGHUP` signal to the service's process ID. + +.PARAMETER Name + The name of the Pode service to restart. + +.PARAMETER Async + Indicates whether to return immediately after issuing the restart command. If not specified, the function waits until the service reaches the 'Running' state. + +.PARAMETER Timeout + The maximum time, in seconds, to wait for the service to reach the 'Running' state when not using `-Async`. Defaults to 10 seconds. + +.PARAMETER Agent + Specifies that only agent-type services should be returned. This parameter is applicable to macOS only. + +.EXAMPLE + Restart-PodeService -Name "MyPodeService" + + Attempts to restart the Pode service named "MyPodeService" on the current platform. + +.EXAMPLE + Restart-PodeService -Name "AnotherService" -Verbose + + Restarts the Pode service named "AnotherService" with detailed verbose output. + +.NOTES + - This function requires administrative/root privileges to execute. + - Platform-specific behavior: + - Windows: Uses `sc.exe control` with the signal `128` to restart the service. + - Linux/macOS: Sends the `SIGHUP` signal to the service process. + - If the service is not running or suspended, no restart signal is sent. + - If the service is not registered, an error is thrown. + - Errors and logs are captured for debugging purposes. + +#> +function Restart-PodeService { + [CmdletBinding(DefaultParameterSetName = 'Default')] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] + $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [int] + $Timeout = 10, + + [switch] + $Agent + ) + Write-Verbose -Message "Attempting to restart service '$Name' on platform $([System.Environment]::OSVersion.Platform)..." + + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege + + try { + + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent + if (!$service) { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + if ('Running' -ne $service.Status ) { + Write-Verbose -Message "Service '$Name' is not Running." + return $false + } + if (Test-PodeIsWindows) { + + Write-Verbose -Message "Sending restart (128) signal to service '$Name'." + if ( Invoke-PodeWinElevatedCommand -Command 'sc control' -Arguments "'$Name' 128") { + if ($Async) { + return $true + } + else { + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout + } + } + throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe control {0} 128', $Name) + + } + elseif ($IsLinux) { + # Start the service + if (((Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP'))) { + if ($Async) { + return $true + } + else { + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout + } + } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $Name) + + } + elseif ($IsMacOS) { + # Start the service + if (((Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP' -Agent:$Agent))) { + if ($Async) { + return $true + } + else { + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout + } + } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $Name) + } + } + catch { + # Log and display the error + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } + + Write-Verbose -Message "Service '$Name' restart operation completed successfully." + return $true +} + + diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 9391e8845..88e4dcb3b 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -873,32 +873,32 @@ function Out-PodeHost { <# .SYNOPSIS -Writes an object to the Host. + Writes an object to the Host. .DESCRIPTION -Writes an object to the Host. -It's advised to use this function, so that any output respects the -Quiet flag of the server. + Writes an object to the Host. + It's advised to use this function, so that any output respects the -Quiet flag of the server. .PARAMETER Object -The object to write. + The object to write. .PARAMETER ForegroundColor -An optional foreground colour. + An optional foreground colour. .PARAMETER NoNewLine -Whether or not to write a new line. + Whether or not to write a new line. .PARAMETER Explode -Show the object content + Show the object content .PARAMETER ShowType -Show the Object Type + Show the Object Type .PARAMETER Label -Show a label for the object + Show a label for the object .PARAMETER Force -Overrides the -Quiet flag of the server. + Overrides the -Quiet flag of the server. .EXAMPLE 'Some output' | Write-PodeHost -ForegroundColor Cyan @@ -908,6 +908,7 @@ function Write-PodeHost { [CmdletBinding(DefaultParameterSetName = 'inbuilt')] param( [Parameter(Position = 0, ValueFromPipeline = $true)] + [Alias('Message')] [object] $Object, diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 new file mode 100644 index 000000000..573ace650 --- /dev/null +++ b/tests/integration/Service.Tests.ps1 @@ -0,0 +1,107 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + + + +Describe 'Service Lifecycle' { + + BeforeAll { + $isAgent = $false + if ($IsMacOS) { + $isAgent = $true + } + } + it 'register' { + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register -Agent:$isAgent + $success | Should -BeTrue + Start-Sleep 10 + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + if ($IsMacOS) { + $status.Status | Should -Be 'Running' + $status.Pid | Should -BeGreaterThan 0 + } + else { + $status.Status | Should -Be 'Stopped' + $status.Pid | Should -Be 0 + } + + $status.Name | Should -Be 'Hello Service' + + } + + + it 'start' -Skip:( $IsMacOS) { + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start -Agent:$isAgent + $success | Should -BeTrue + Start-Sleep 2 + $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + $status.Status | Should -Be 'Running' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + $webRequest.Content | Should -Be 'Hello, Service!' + } + + it 'pause' { + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend -Agent:$isAgent + $success | Should -BeTrue + Start-Sleep 2 + + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + $status.Status | Should -Be 'Suspended' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + { Invoke-WebRequest -uri http://localhost:8080 } | Should -Throw + } + + it 'resume' { + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume -Agent:$isAgent + $success | Should -BeTrue + Start-Sleep 2 + $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + $status.Status | Should -Be 'Running' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + $webRequest.Content | Should -Be 'Hello, Service!' + } + it 'stop' { + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop -Agent:$isAgent + $success | Should -BeTrue + Start-Sleep 2 + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + $status.Status | Should -Be 'Stopped' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -Be 0 + + { Invoke-WebRequest -uri http://localhost:8080 } | Should -Throw + } + + it 're-start' { + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start -Agent:$isAgent + $success | Should -BeTrue + Start-Sleep 2 + $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + $status.Status | Should -Be 'Running' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + $webRequest.Content | Should -Be 'Hello, Service!' + } + + + + it 'unregister' { + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + $status.Status | Should -Be 'Running' + $isAgent = $status.Type -eq 'Agent' + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Unregister -Force -Agent:$isAgent + $success | Should -BeTrue + Start-Sleep 2 + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent + $status | Should -BeNullOrEmpty + { Invoke-WebRequest -uri http://localhost:8080 } | Should -Throw + } + +} \ No newline at end of file diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 63949efdc..51330e98f 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -29,7 +29,7 @@ Describe 'Add-PodeEndpoint' { Context 'Valid parameters supplied' { BeforeAll { Mock Test-PodeIPAddress { return $true } - Mock Test-PodeIsAdminUser { return $true } + Mock Test-PodeAdminPrivilege { return $true } } It 'Set just a Hostname address - old' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } @@ -371,7 +371,7 @@ Describe 'Add-PodeEndpoint' { } It 'Throws an error for not running as admin' { - Mock Test-PodeIsAdminUser { return $false } + Mock Test-PodeAdminPrivilege { return $false } $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } { Add-PodeEndpoint -Address '127.0.0.2' -Protocol 'HTTP' } | Should -Throw -ExpectedMessage $PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage #'*Must be running with admin*' } @@ -381,7 +381,7 @@ Describe 'Add-PodeEndpoint' { Describe 'Get-PodeEndpoint' { BeforeAll { Mock Test-PodeIPAddress { return $true } - Mock Test-PodeIsAdminUser { return $true } } + Mock Test-PodeAdminPrivilege { return $true } } It 'Returns no Endpoints' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } diff --git a/tests/unit/Routes.Tests.ps1 b/tests/unit/Routes.Tests.ps1 index b5f746448..118ef18ff 100644 --- a/tests/unit/Routes.Tests.ps1 +++ b/tests/unit/Routes.Tests.ps1 @@ -874,7 +874,7 @@ Describe 'Get-PodeRouteByUrl' { Describe 'Get-PodeRoute' { BeforeAll { Mock Test-PodeIPAddress { return $true } - Mock Test-PodeIsAdminUser { return $true } } + Mock Test-PodeAdminPrivilege { return $true } } BeforeEach { $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; }; 'FindEndpoints' = @{}; 'Endpoints' = @{}; 'EndpointsMap' = @{}; 'Type' = $null 'OpenAPI' = @{ diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 new file mode 100644 index 000000000..a198585e4 --- /dev/null +++ b/tests/unit/Service.Tests.ps1 @@ -0,0 +1,237 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} + + +Describe 'Register-PodeService' { + BeforeAll { + Mock -CommandName Confirm-PodeAdminPrivilege + Mock -CommandName Register-PodeMonitorWindowsService { return $true } + Mock -CommandName Register-PodeLinuxService { return $true } + Mock -CommandName Register-PodeMacService { return $true } + Mock -CommandName Start-PodeService { return $true } + Mock -CommandName New-Item + Mock -CommandName ConvertTo-Json + Mock -CommandName Set-Content + Mock -CommandName Get-Process + Mock -CommandName Get-Module { return @{ModuleBase = $pwd } } + } + + + Context 'With valid parameters' { + + + It 'Registers a Windows service successfully' -Skip:(!$IsWindows) { + + # Arrange + $params = @{ + Name = 'TestService' + Description = 'Test Description' + DisplayName = 'Test Service Display Name' + StartupType = 'Automatic' + ParameterString = '-Verbose' + LogServicePodeHost = $true + Start = $true + } + # Mock -CommandName (Get-Process -Id $PID).Path -MockWith { 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' } + + # Act + Register-PodeService @params + + # Assert + Assert-MockCalled -CommandName Confirm-PodeAdminPrivilege -Exactly 1 + Assert-MockCalled -CommandName Register-PodeMonitorWindowsService -Exactly 1 + Assert-MockCalled -CommandName Start-PodeService -Exactly 1 + } + + It 'Registers a Linux service successfully' -Skip:(!$IsLinux) { + + $params = @{ + Name = 'LinuxTestService' + Description = 'Linux Test Service' + Start = $true + } + + # Act + Register-PodeService @params + + # Assert + Assert-MockCalled -CommandName Register-PodeLinuxService -Exactly 1 + Assert-MockCalled -CommandName Start-PodeService -Exactly 1 + } + + It 'Registers a macOS service successfully' -Skip:(!$IsMacOS) { + # Arrange + $params = @{ + Name = 'MacTestService' + Description = 'macOS Test Service' + Start = $true + } + + # Act + Register-PodeService @params + + # Assert + Assert-MockCalled -CommandName Register-PodeMacService -Exactly 1 + Assert-MockCalled -CommandName Start-PodeService -Exactly 1 + } + } + + Context 'With invalid parameters' { + It 'Throws an error if Name is missing' { + # Act & Assert + { Register-PodeService -Name $null -Description 'Missing Name' } | Should -Throw + } + + It 'Throws an error if Password is missing for a specified WindowsUser' -Skip:(!$IsWindows) { + # Arrange + $params = @{ + Name = 'TestService' + WindowsUser = 'TestUser' + } + + # Act & Assert + Register-PodeService @params -ErrorAction SilentlyContinue | Should -BeFalse + } + } + +} +Describe 'Start-PodeService' { + BeforeAll { + # Mock the required commands + Mock -CommandName Confirm-PodeAdminPrivilege + Mock -CommandName Invoke-PodeWinElevatedCommand + Mock -CommandName Test-PodeLinuxServiceIsRegistered + Mock -CommandName Test-PodeLinuxServiceIsActive + Mock -CommandName Start-PodeLinuxService + Mock -CommandName Test-PodeMacOsServiceIsRegistered + Mock -CommandName Test-PodeMacOsServiceIsActive + Mock -CommandName Start-PodeMacOsService + Mock -CommandName Write-PodeErrorLog + Mock -CommandName Write-Error + Mock -CommandName Get-PodeServiceStatus { return @{Status = '' } } + } + + Context 'On Windows platform' { + It 'Starts a stopped service successfully' -Skip:(!$IsWindows) { + # Mock a stopped service and simulate it starting + $script:status = 'none' + Mock -CommandName Get-PodeServiceStatus -MockWith { + if ($script:status -eq 'none') { + $script:status = 'Stopped' + } + else { + $script:status = 'Running' + } + [pscustomobject]@{ Name = 'TestService'; Status = $status } + } + Mock -CommandName Wait-PodeServiceStatus { $true } + Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { $true } + + # Act + Start-PodeService -Name 'TestService' | Should -Be $true + + # Assert + Assert-MockCalled -CommandName Invoke-PodeWinElevatedCommand -Exactly 1 + } + + It 'Starts a started service ' -Skip:(!$IsWindows) { + Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { $null } + Mock -CommandName Get-PodeServiceStatus -MockWith { + [pscustomobject]@{ Name = 'TestService'; Status = 'Running' } + } + + # Act + Start-PodeService -Name 'TestService' | Should -Be $true + + # Assert + Assert-MockCalled -CommandName Invoke-PodeWinElevatedCommand -Exactly 0 + } + + + It 'Throws an error if the service is not registered' -Skip:(!$IsWindows) { + + Start-PodeService -Name 'NonExistentService' -ErrorAction SilentlyContinue | Should -BeFalse + } + } + + Context 'On Linux platform' { + It 'Starts a stopped service successfully' -Skip:(!$IsLinux) { + $script:status = 'none' + Mock -CommandName Get-PodeServiceStatus -MockWith { + if ($script:status -eq 'none') { + $script:status = 'Stopped' + } + else { + $script:status = 'Running' + } + [pscustomobject]@{ Name = 'TestService'; Status = $status } + } + Mock -CommandName Wait-PodeServiceStatus { $true } + Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $true } + Mock -CommandName Start-PodeLinuxService -MockWith { $true } + + # Act + Start-PodeService -Name 'TestService' | Should -Be $true + + # Assert + Assert-MockCalled -CommandName Start-PodeLinuxService -Exactly 1 + } + + It 'Starts a started service ' -Skip:(!$IsLinux) { + + Mock -CommandName Start-PodeLinuxService -MockWith { $true } + Mock -CommandName Get-PodeServiceStatus -MockWith { + [pscustomobject]@{ Name = 'TestService'; Status = 'Running' } + } + + + # Act + Start-PodeService -Name 'TestService' | Should -Be $true + + # Assert + Assert-MockCalled -CommandName Start-PodeLinuxService -Exactly 0 + } + + It 'Return false if the service is not registered' -Skip:(!$IsLinux) { + Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $false } + Start-PodeService -Name 'NonExistentService' | Should -BeFalse + } + } + + Context 'On macOS platform' { + It 'Starts a stopped service successfully' -Skip:(!$IsMacOS) { + $script:status = 'none' + Mock -CommandName Get-PodeServiceStatus -MockWith { + if ($script:status -eq 'none') { + $script:status = 'Stopped' + } + else { + $script:status = 'Running' + } + [pscustomobject]@{ Name = 'TestService'; Status = $status } + } + Mock -CommandName Wait-PodeServiceStatus { $true } + Mock -CommandName Test-PodeMacOsServiceIsRegistered -MockWith { $true } + Mock -CommandName Start-PodeMacOsService -MockWith { $true } + + # Act + Start-PodeService -Name 'MacService' | Should -Be $true + + # Assert + Assert-MockCalled -CommandName Start-PodeMacOsService -Exactly 1 + } + + It 'Return false if the service is not registered' -Skip:(!$IsMacOS) { + Mock -CommandName Test-PodeMacOsServiceIsRegistered -MockWith { $false } + + Start-PodeService -Name 'NonExistentService' | Should -BeFalse + } + } +} +