From f264e07e7938c94e6643d9b0588a82570618d9e3 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 15 Oct 2024 18:39:03 -0700 Subject: [PATCH 01/93] First drop --- src/Monitor/NuGet.config | 6 + src/Monitor/PodeMain.cs | 20 ++ src/Monitor/PodePwshMonitor.cs | 254 ++++++++++++++++++++++ src/Monitor/PodePwshMonitorService.csproj | 18 ++ src/Monitor/PodeWorker.cs | 42 ++++ src/Private/Context.ps1 | 19 +- src/Private/Runspaces.ps1 | 1 - src/Private/Server.ps1 | 2 + src/Private/Service.ps1 | 223 +++++++++++++++++++ src/Public/Core.ps1 | 46 ++-- 10 files changed, 616 insertions(+), 15 deletions(-) create mode 100644 src/Monitor/NuGet.config create mode 100644 src/Monitor/PodeMain.cs create mode 100644 src/Monitor/PodePwshMonitor.cs create mode 100644 src/Monitor/PodePwshMonitorService.csproj create mode 100644 src/Monitor/PodeWorker.cs create mode 100644 src/Private/Service.ps1 diff --git a/src/Monitor/NuGet.config b/src/Monitor/NuGet.config new file mode 100644 index 000000000..83a2e372f --- /dev/null +++ b/src/Monitor/NuGet.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Monitor/PodeMain.cs b/src/Monitor/PodeMain.cs new file mode 100644 index 000000000..bcf17a4b1 --- /dev/null +++ b/src/Monitor/PodeMain.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace PodePwshMonitorService +{ + public class Program + { + public static void Main(string[] args) + { + Host.CreateDefaultBuilder(args) + .UseWindowsService() // For running as a Windows service + .ConfigureServices(services => + { + services.AddHostedService(); // Add your worker service here + }) + .Build() + .Run(); + } + } +} diff --git a/src/Monitor/PodePwshMonitor.cs b/src/Monitor/PodePwshMonitor.cs new file mode 100644 index 000000000..d24d10c02 --- /dev/null +++ b/src/Monitor/PodePwshMonitor.cs @@ -0,0 +1,254 @@ +/* + * PodePwshMonitorService + * + * This service monitors and controls the execution of a PowerShell process using named pipes for communication. + * + * SC Command Reference for Managing Windows Services: + * + * Install Service: + * sc create PodePwshMonitorService binPath= "C:\path\to\your\service\PodePwshMonitorService.exe" start= auto + * + * Start Service: + * sc start PodePwshMonitorService + * + * Stop Service: + * sc stop PodePwshMonitorService + * + * Delete Service: + * sc delete PodePwshMonitorService + * + * Query Service Status: + * sc query PodePwshMonitorService + * + * Configure Service to Restart on Failure: + * sc failure PodePwshMonitorService reset= 0 actions= restart/60000 + * + * Example for running the service: + * sc start PodePwshMonitorService + * sc stop PodePwshMonitorService + * sc delete PodePwshMonitorService + * + */ +using System; +using Microsoft.Extensions.Hosting; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; +using System.Threading; + +namespace PodePwshMonitorService +{ + public class PodePwshMonitor + { + private Process _powerShellProcess; + private readonly string _scriptPath; + private readonly string _parameterString; + private string _pwshPath; + + private readonly bool _quiet; + private readonly bool _disableTermination; + private readonly int _shutdownWaitTimeMs; + private string _pipeName; + private NamedPipeClientStream _pipeClient; // Changed to client stream + + private readonly string _logFilePath = "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloWorld\\PodePwshMonitorService.log"; // Path to log file + + public PodePwshMonitor(string scriptPath, string pwshPath, string parameterString = "", bool quiet = true, bool disableTermination = true, int shutdownWaitTimeMs = 30000) + { + // Initialize fields with constructor arguments + _scriptPath = scriptPath; // Path to the PowerShell script to be executed + _pwshPath = pwshPath; // Path to the PowerShell executable (pwsh) + _parameterString = parameterString; // Additional parameters to pass to the script (if any) + _disableTermination = disableTermination; // Flag to disable termination of the service + _quiet = quiet; // Flag to suppress output for a quieter service + _shutdownWaitTimeMs = shutdownWaitTimeMs; // Maximum wait time before forcefully shutting down the process + + // Dynamically generate a unique PipeName for communication + _pipeName = $"PodePipe_{Guid.NewGuid()}"; // Generate a unique pipe name to avoid conflicts + } + + public void StartPowerShellProcess() + { + if (_powerShellProcess == null || _powerShellProcess.HasExited) + { + try + { + // Define the PowerShell process + _powerShellProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = _pwshPath, // Set the PowerShell executable path (pwsh) + RedirectStandardOutput = true, // Redirect standard output + RedirectStandardError = true, // Redirect standard error + UseShellExecute = false, // Do not use shell execution + CreateNoWindow = true // Do not create a new window + } + }; + + // Properly escape double quotes within the JSON string + string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; + + // Build the PowerShell command with NoProfile and global variable initialization + string command = $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; + + Log($"Starting PowerShell process with command: {command}"); + + // Set the arguments for the PowerShell process + _powerShellProcess.StartInfo.Arguments = command; + + // Start the process + _powerShellProcess.Start(); + + // Log output and error asynchronously + _powerShellProcess.OutputDataReceived += (sender, args) => Log(args.Data); + _powerShellProcess.ErrorDataReceived += (sender, args) => Log(args.Data); + _powerShellProcess.BeginOutputReadLine(); + _powerShellProcess.BeginErrorReadLine(); + + Log("PowerShell process started successfully."); + } + catch (Exception ex) + { + Log($"Failed to start PowerShell process: {ex.Message}"); + } + } + else + { + Log("PowerShell process is already running."); + } + } + + + public void StopPowerShellProcess() + { + try + { + _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); + Log($"Connecting to the pipe server using pipe: {_pipeName}"); + + // Connect to the PowerShell pipe server + _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection + + if (_pipeClient.IsConnected) + { + // Send shutdown message and wait for the process to exit + SendPipeMessage("shutdown"); + Log($"Waiting up to {_shutdownWaitTimeMs} milliseconds for the PowerShell process to exit..."); + + // Timeout logic + int waited = 0; + int interval = 200; // Check every 200ms + + while (!_powerShellProcess.HasExited && waited < _shutdownWaitTimeMs) + { + Thread.Sleep(interval); + waited += interval; + } + + if (_powerShellProcess.HasExited) + { + Log("PowerShell process has been shutdown gracefully."); + } + else + { + Log($"PowerShell process did not exit in {_shutdownWaitTimeMs} milliseconds."); + } + } + else + { + Log($"Failed to connect to the PowerShell pipe server using pipe: {_pipeName}"); + } + + // Forcefully kill the process if it's still running + if (_powerShellProcess != null && !_powerShellProcess.HasExited) + { + try + { + _powerShellProcess.Kill(); + Log("PowerShell process killed successfully."); + } + catch (Exception ex) + { + Log($"Error killing PowerShell process: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Log($"Error stopping PowerShell process: {ex.Message}"); + } + finally + { + // Clean up the named pipe client and process + _powerShellProcess?.Dispose(); + _powerShellProcess = null; + _pipeClient?.Dispose(); + Log("PowerShell process and pipe client disposed."); + } + } + + + public void RestartPowerShellProcess() + { + // Simply send the restart message, no need to stop and start again + if (_pipeClient != null && _pipeClient.IsConnected) + { + SendPipeMessage("restart"); // Inform PowerShell about the restart + Log("Restart message sent to PowerShell."); + } + } + + + private void SendPipeMessage(string message) + { + if (_pipeClient == null) + { + Log("Pipe client is not initialized, cannot send message."); + return; + } + + if (!_pipeClient.IsConnected) + { + Log("Pipe client is not connected, cannot send message."); + return; + } + + try + { + // Send the message using the pipe client stream + using (var writer = new StreamWriter(_pipeClient, leaveOpen: true)) // leaveOpen to keep the pipe alive for multiple writes + { + writer.AutoFlush = true; + writer.WriteLine(message); + Log($"Message sent to PowerShell: {message}"); + } + } + catch (Exception ex) + { + Log($"Failed to send message to PowerShell: {ex.Message}"); + } + } + + + private void Log(string data) + { + if (!string.IsNullOrEmpty(data)) + { + try + { + // Write log entry to file, create the file if it doesn't exist + using (StreamWriter writer = new StreamWriter(_logFilePath, true)) + { + writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {data}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to log to file: {ex.Message}"); + } + } + } + } +} diff --git a/src/Monitor/PodePwshMonitorService.csproj b/src/Monitor/PodePwshMonitorService.csproj new file mode 100644 index 000000000..9bb236cc7 --- /dev/null +++ b/src/Monitor/PodePwshMonitorService.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + true + true + win-x64 + + + + + + + + + + diff --git a/src/Monitor/PodeWorker.cs b/src/Monitor/PodeWorker.cs new file mode 100644 index 000000000..520ae5be4 --- /dev/null +++ b/src/Monitor/PodeWorker.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +namespace PodePwshMonitorService +{ + public class PodeWorker : BackgroundService + { + private readonly ILogger _logger; + private PodePwshMonitor _pwshMonitor; + + public PodeWorker(ILogger logger) + { + _logger = logger; + string scriptPath = @"C:\Users\m_dan\Documents\GitHub\Pode\examples\HelloWorld\HelloWorld.ps1"; // Update with your script path + string pwshPath = @"C:\Program Files\PowerShell\7\pwsh.exe"; // Update with your PowerShell path + _pwshMonitor = new PodePwshMonitor(scriptPath, pwshPath,"", false); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("PodeWorker running at: {time}", DateTimeOffset.Now); + + while (!stoppingToken.IsCancellationRequested) + { + _pwshMonitor.StartPowerShellProcess(); + + await Task.Delay(10000, stoppingToken); // Monitor every 10 seconds + } + } + + public override Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Service is stopping at: {time}", DateTimeOffset.Now); + _pwshMonitor.StopPowerShellProcess(); + return base.StopAsync(stoppingToken); + } + } +} \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 4e38b56d8..c1329c139 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -50,7 +50,10 @@ function New-PodeContext { $Quiet, [switch] - $EnableBreakpoints + $EnableBreakpoints, + + [hashtable] + $Service ) # set a random server name if one not supplied @@ -96,6 +99,10 @@ function New-PodeContext { $ctx.Server.Quiet = $Quiet.IsPresent $ctx.Server.ComputerName = [System.Net.DNS]::GetHostName() + + if ($null -ne $Service) { + $ctx.Server.Service = $Service + } # list of created listeners/receivers $ctx.Listeners = @() $ctx.Receivers = @() @@ -144,6 +151,7 @@ function New-PodeContext { Tasks = 2 WebSockets = 2 Timers = 1 + Service = 0 } # set socket details for pode server @@ -435,6 +443,7 @@ function New-PodeContext { Tasks = $null Files = $null Timers = $null + Service =$null } # threading locks, etc. @@ -630,6 +639,14 @@ 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' + } + } } <# diff --git a/src/Private/Runspaces.ps1 b/src/Private/Runspaces.ps1 index 7bedb2f33..336e1b821 100644 --- a/src/Private/Runspaces.ps1 +++ b/src/Private/Runspaces.ps1 @@ -40,7 +40,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 ea4fa1f87..5f62f9791 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -66,6 +66,8 @@ function Start-PodeInternalServer { # start runspace for loggers Start-PodeLoggingRunspace + Start-PodeServiceHearthbeat + # start runspace for schedules Start-PodeScheduleRunspace diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 new file mode 100644 index 000000000..7b9fbdbbd --- /dev/null +++ b/src/Private/Service.ps1 @@ -0,0 +1,223 @@ +<# +.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. + +.EXAMPLE + Test-PodeServiceEnabled + + Returns `$true` if the Pode service is enabled, otherwise returns `$false`. + +.RETURNS + [Bool] - `$true` if the 'Service' key exists, `$false` if it does not. + +.NOTES + This function simply checks the existence of the 'Service' key in `$PodeContext.Server` to determine if the service is enabled. +#> +function Test-PodeServiceEnabled { + + # Check if the 'Service' key exists in the $PodeContext.Server hashtable + return $PodeContext.Server.ContainsKey('Service') +} + + +#$global:PodeService=@{DisableTermination=$true;Quiet=$false;Pipename='ssss'} +<# +.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-PodeServiceHearthbeat + + This command starts the Pode service monitoring and waits for 'shutdown' or 'restart' commands from the named pipe. + +.NOTES + 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 'shutdown' message, the Pode server is stopped. + If the pipe receives a 'restart' message, the Pode server is restarted. + +.AUTHOR + Your Name +#> + +function Start-PodeServiceHearthbeat { + + # 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 = { + Write-PodeServiceLog -Message "Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" + + try { + # Create a named pipe server stream + $pipeStream = [System.IO.Pipes.NamedPipeServerStream]::new( + $PodeContext.Server.Service.PipeName, + [System.IO.Pipes.PipeDirection]::InOut, + 2, # Max number of allowed concurrent connections + [System.IO.Pipes.PipeTransmissionMode]::Message, + [System.IO.Pipes.PipeOptions]::None + ) + + Write-PodeServiceLog -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." + $pipeStream.WaitForConnection() # Wait until a client connects + Write-PodeServiceLog -Message "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 + while ($pipeStream.IsConnected) { + $message = $reader.ReadLine() # Read message from the pipe + + if ($message) { + Write-PodeServiceLog -Message "Received message: $message" + + # Process 'shutdown' message + if ($message -eq 'shutdown') { + Write-PodeServiceLog -Message 'Server requested shutdown. Closing client...' + Close-PodeServer # Gracefully stop the Pode server + break # Exit the loop + + # Process 'restart' message + } elseif ($message -eq 'restart') { + Write-PodeServiceLog -Message 'Server requested restart. Restarting client...' + Restart-PodeServer # Restart the Pode server + break # Exit the loop + } + } + } + } + catch { + $_ | Write-PodeServiceLog # Log any errors that occur during pipe operation + } + finally { + $pipeStream.Dispose() # Always dispose of the pipe stream when done + } + } + + # Assign a name to the Pode service + $PodeContext.Server.Service['Name'] = 'Service' + Write-PodeServiceLog -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 + } + else { + # Log when the service is not enabled + Write-PodeServiceLog -Message 'Service is not working' + Write-PodeServiceLog -Message ($PodeService | ConvertTo-Json -Compress) + } +} + + + +function Write-PodeServiceLog { + [CmdletBinding(DefaultParameterSetName = 'Message')] + param( + + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')] + [System.Exception] + $Exception, + + [Parameter(ParameterSetName = 'Exception')] + [switch] + $CheckInnerException, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Error')] + [System.Management.Automation.ErrorRecord] + $ErrorRecord, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Message')] + [string] + $Message, + + [string] + $Level = 'Informational', + + [string] + $Tag = '-', + + [Parameter()] + [int] + $ThreadId + + ) + Process { + $Service = $PodeContext.Server.Service + if ($null -eq $Service ) { + $Service = @{Name = 'Not a service' } + } + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + + 'message' { + $logItem = @{ + Name = $Service.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Level = $Level + Message = $Message + Tag = $Tag + } + } + break + } + 'custom' { + $logItem = @{ + Name = $Service.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Level = $Level + Message = $Message + Tag = $Tag + } + } + break + } + 'exception' { + $logItem = @{ + Name = $Service.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Category = $Exception.Source + Message = $Exception.Message + StackTrace = $Exception.StackTrace + Level = $Level + } + } + Write-PodeErrorLog -Level $Level -CheckInnerException:$CheckInnerException -Exception $Exception + } + + 'error' { + $logItem = @{ + Name = $Service.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Category = $ErrorRecord.CategoryInfo.ToString() + Message = $ErrorRecord.Exception.Message + StackTrace = $ErrorRecord.ScriptStackTrace + Level = $Level + } + } + Write-PodeErrorLog -Level $Level -ErrorRecord $ErrorRecord + } + } + + $lpath = Get-PodeRelativePath -Path './logs' -JoinRoot + $logItem | ConvertTo-Json -Compress -Depth 5 | Add-Content "$lpath/watchdog-$($Service.Name).log" + + } +} \ No newline at end of file diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 3d609f763..a2504976c 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -150,6 +150,23 @@ function Start-PodeServer { $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 + ) { + $DisableTermination = [switch]$PodeService.DisableTermination + $Quiet = [switch]$PodeService.Quiet + + $monitorService = @{ + DisableTermination = $PodeService.DisableTermination + Quiet = $PodeService.Quiet + PipeName = $PodeService.PipeName + } + } + } + try { # if we have a filepath, resolve it - and extract a root path from it if ($PSCmdlet.ParameterSetName -ieq 'file') { @@ -171,20 +188,23 @@ function Start-PodeServer { $RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath } + $params = @{ + ScriptBlock = $ScriptBlock + FilePath = $FilePath + Threads = $Threads + Interval = $Interval + ServerRoot = $(Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) + ServerlessType = $ServerlessType + ListenerType = $ListenerType + EnablePool = $EnablePool + StatusPageExceptions = $StatusPageExceptions + DisableTermination = $DisableTermination + Quiet = $Quiet + EnableBreakpoints = $EnableBreakpoints + Service = $monitorService + } # create main context object - $PodeContext = New-PodeContext ` - -ScriptBlock $ScriptBlock ` - -FilePath $FilePath ` - -Threads $Threads ` - -Interval $Interval ` - -ServerRoot (Protect-PodeValue -Value $RootPath -Default $MyInvocation.PSScriptRoot) ` - -ServerlessType $ServerlessType ` - -ListenerType $ListenerType ` - -EnablePool $EnablePool ` - -StatusPageExceptions $StatusPageExceptions ` - -DisableTermination:$DisableTermination ` - -Quiet:$Quiet ` - -EnableBreakpoints:$EnableBreakpoints + $PodeContext = New-PodeContext @params # set it so ctrl-c can terminate, unless serverless/iis, or disabled if (!$PodeContext.Server.DisableTermination -and ($null -eq $psISE)) { From f965cbf1bde4002615149d9aba9e3f3808ddee85 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 16 Oct 2024 07:51:08 -0700 Subject: [PATCH 02/93] Integrated in Pode --- examples/HelloService/HelloService.ps1 | 92 ++++++++++ examples/HelloService/srvsettings.json | 11 ++ examples/HelloWorld/servicesettings.json | 11 ++ pode.build.ps1 | 54 +++++- src/Monitor/NuGet.config | 6 - src/Monitor/PodeMain.cs | 20 --- src/Monitor/PodeWorker.cs | 42 ----- src/Pode.psd1 | 6 +- .../PodeMonitor.csproj} | 1 + src/PodePwshMonitor/PodePwshMain.cs | 36 ++++ .../PodePwshMonitor.cs | 46 +++-- src/PodePwshMonitor/PodePwshWorker.cs | 67 +++++++ src/PodePwshMonitor/PodePwshWorkerOptions.cs | 20 +++ src/Private/Service.ps1 | 7 +- src/Public/Service.ps1 | 169 ++++++++++++++++++ 15 files changed, 500 insertions(+), 88 deletions(-) create mode 100644 examples/HelloService/HelloService.ps1 create mode 100644 examples/HelloService/srvsettings.json create mode 100644 examples/HelloWorld/servicesettings.json delete mode 100644 src/Monitor/NuGet.config delete mode 100644 src/Monitor/PodeMain.cs delete mode 100644 src/Monitor/PodeWorker.cs rename src/{Monitor/PodePwshMonitorService.csproj => PodePwshMonitor/PodeMonitor.csproj} (89%) create mode 100644 src/PodePwshMonitor/PodePwshMain.cs rename src/{Monitor => PodePwshMonitor}/PodePwshMonitor.cs (84%) create mode 100644 src/PodePwshMonitor/PodePwshWorker.cs create mode 100644 src/PodePwshMonitor/PodePwshWorkerOptions.cs create mode 100644 src/Public/Service.ps1 diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 new file mode 100644 index 000000000..8cef7d3ef --- /dev/null +++ b/examples/HelloService/HelloService.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS + PowerShell script to set up a Pode server with a simple GET endpoint. + +.DESCRIPTION + This script sets up a Pode server that listens on port 8080. It includes a single route for GET requests + to the root path ('/') that returns a simple text response. + +.EXAMPLE + To run the sample: ./HelloWorld/HelloWorld.ps1 + + # HTML responses 'Hello, world! + Invoke-RestMethod -Uri http://localhost:8081/ -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/HelloWorld/HelloWorld.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +param( + [switch] + $Register, + [switch] + $Unregister, + [switch] + $Start, + [switch] + $Stop, + [switch] + $Query +) +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) { + Register-PodeService -Name 'HelloService' + exit +} +if ($Unregister.IsPresent) { + Unregister-PodeService -Name 'HelloService' + exit +} +if ($Start.IsPresent) { + Start-Service -Name 'HelloService' + exit +} + +if ($Stop.IsPresent) { + Stop-Service -Name 'HelloService' + exit +} + +if ($Query.IsPresent) { + Get-Service -Name 'HelloService' + exit +} +# Alternatively, you can directly import the Pode module from the system +# Import-Module Pode + +# Start the Pode server +Start-PodeServer { + # Add an HTTP endpoint listening on localhost at port 8080 + Add-PodeEndpoint -Address localhost -Port 8080 -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/srvsettings.json b/examples/HelloService/srvsettings.json new file mode 100644 index 000000000..5c788b9c9 --- /dev/null +++ b/examples/HelloService/srvsettings.json @@ -0,0 +1,11 @@ +{ + "PodePwshWorker": { + "ParameterString": "", + "Quiet": true, + "ScriptPath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", + "DisableTermination": true, + "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + "ShutdownWaitTimeMs": 30000, + "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService_svc.log" + } +} diff --git a/examples/HelloWorld/servicesettings.json b/examples/HelloWorld/servicesettings.json new file mode 100644 index 000000000..6d5e57be6 --- /dev/null +++ b/examples/HelloWorld/servicesettings.json @@ -0,0 +1,11 @@ +{ + "PodePwshWorker ": { + "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\\PodePwshMonitorService.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 34a72f0ce..c6d703716 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -141,6 +141,37 @@ function Invoke-PodeBuildDotnetBuild($target) { } + +function Invoke-PodeBuildDotnetMonitorSrvBuild() { + # Retrieve the highest installed SDK version + $majorVersion = ([version](dotnet --version)).Major + + # Determine if the target framework is compatible + $isCompatible = $majorVersion -ge 8 + + # Skip build if not compatible + if ($isCompatible) { + Write-Host "SDK for target framework $target is compatible with the installed SDKs" + } + else { + Write-Host "SDK for target framework $target is not compatible with the installed SDKs. Skipping build." + return + } + if ($Version) { + Write-Host "Assembly Version $Version" + $AssemblyVersion = "-p:Version=$Version" + } + else { + $AssemblyVersion = '' + } + + dotnet publish --configuration Release $AssemblyVersion --output ../Bin/$target + if (!$?) { + throw "dotnet publish failed for $($target)" + } + +} + function Get-PodeBuildPwshEOL { $eol = Invoke-RestMethod -Uri 'https://endoflife.date/api/powershell.json' -Headers @{ Accept = 'application/json' } return @{ @@ -401,6 +432,21 @@ Task Build BuildDeps, { finally { Pop-Location } + + if (Test-Path ./src/Bin) { + Remove-Item -Path ./src/Bin -Recurse -Force | Out-Null + } + + try { + Push-Location ./src/PodePwshMonitor + Invoke-PodeBuildDotnetMonitorSrvBuild + } + finally { + Pop-Location + } + + + } @@ -474,7 +520,7 @@ Task Pack 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 @@ -675,6 +721,12 @@ Task CleanLibs { Write-Host "Removing $path contents" 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" } diff --git a/src/Monitor/NuGet.config b/src/Monitor/NuGet.config deleted file mode 100644 index 83a2e372f..000000000 --- a/src/Monitor/NuGet.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Monitor/PodeMain.cs b/src/Monitor/PodeMain.cs deleted file mode 100644 index bcf17a4b1..000000000 --- a/src/Monitor/PodeMain.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace PodePwshMonitorService -{ - public class Program - { - public static void Main(string[] args) - { - Host.CreateDefaultBuilder(args) - .UseWindowsService() // For running as a Windows service - .ConfigureServices(services => - { - services.AddHostedService(); // Add your worker service here - }) - .Build() - .Run(); - } - } -} diff --git a/src/Monitor/PodeWorker.cs b/src/Monitor/PodeWorker.cs deleted file mode 100644 index 520ae5be4..000000000 --- a/src/Monitor/PodeWorker.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -namespace PodePwshMonitorService -{ - public class PodeWorker : BackgroundService - { - private readonly ILogger _logger; - private PodePwshMonitor _pwshMonitor; - - public PodeWorker(ILogger logger) - { - _logger = logger; - string scriptPath = @"C:\Users\m_dan\Documents\GitHub\Pode\examples\HelloWorld\HelloWorld.ps1"; // Update with your script path - string pwshPath = @"C:\Program Files\PowerShell\7\pwsh.exe"; // Update with your PowerShell path - _pwshMonitor = new PodePwshMonitor(scriptPath, pwshPath,"", false); - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("PodeWorker running at: {time}", DateTimeOffset.Now); - - while (!stoppingToken.IsCancellationRequested) - { - _pwshMonitor.StartPowerShellProcess(); - - await Task.Delay(10000, stoppingToken); // Monitor every 10 seconds - } - } - - public override Task StopAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Service is stopping at: {time}", DateTimeOffset.Now); - _pwshMonitor.StopPowerShellProcess(); - return base.StopAsync(stoppingToken); - } - } -} \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index ad02ac21c..a63533cae 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -498,7 +498,11 @@ 'Test-PodeScopedVariable', 'Clear-PodeScopedVariables', 'Get-PodeScopedVariable', - 'Use-PodeScopedVariables' + 'Use-PodeScopedVariables', + + # service + 'Register-PodeService', + 'Unregister-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/Monitor/PodePwshMonitorService.csproj b/src/PodePwshMonitor/PodeMonitor.csproj similarity index 89% rename from src/Monitor/PodePwshMonitorService.csproj rename to src/PodePwshMonitor/PodeMonitor.csproj index 9bb236cc7..488deb338 100644 --- a/src/Monitor/PodePwshMonitorService.csproj +++ b/src/PodePwshMonitor/PodeMonitor.csproj @@ -9,6 +9,7 @@ + diff --git a/src/PodePwshMonitor/PodePwshMain.cs b/src/PodePwshMonitor/PodePwshMain.cs new file mode 100644 index 000000000..1353ea8e0 --- /dev/null +++ b/src/PodePwshMonitor/PodePwshMain.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using System; + +namespace Pode.Services +{ + public class Program + { + public static void Main(string[] args) + { + var customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Custom config file from args or default + + try + { + Host.CreateDefaultBuilder(args) + .UseWindowsService() // For running as a Windows service + .ConfigureAppConfiguration((context, config) => + { + config.AddJsonFile(customConfigFile, optional: false, reloadOnChange: true); + }) + .ConfigureServices((context, services) => + { + services.Configure(context.Configuration.GetSection("PodePwshWorker")); + services.AddHostedService(); + }) + .Build() + .Run(); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } + } + } +} diff --git a/src/Monitor/PodePwshMonitor.cs b/src/PodePwshMonitor/PodePwshMonitor.cs similarity index 84% rename from src/Monitor/PodePwshMonitor.cs rename to src/PodePwshMonitor/PodePwshMonitor.cs index d24d10c02..006f54d0a 100644 --- a/src/Monitor/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/PodePwshMonitor.cs @@ -37,7 +37,7 @@ using System.Threading.Tasks; using System.Threading; -namespace PodePwshMonitorService +namespace Pode.Services { public class PodePwshMonitor { @@ -51,11 +51,13 @@ public class PodePwshMonitor private readonly int _shutdownWaitTimeMs; private string _pipeName; private NamedPipeClientStream _pipeClient; // Changed to client stream + private DateTime _lastLogTime; - private readonly string _logFilePath = "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloWorld\\PodePwshMonitorService.log"; // Path to log file + private readonly string _logFilePath; // Path to log file - public PodePwshMonitor(string scriptPath, string pwshPath, string parameterString = "", bool quiet = true, bool disableTermination = true, int shutdownWaitTimeMs = 30000) + public PodePwshMonitor(string scriptPath, string pwshPath, string parameterString = "", string logFilePath = ".\\PodePwshMonitorService.log", bool quiet = true, bool disableTermination = true, int shutdownWaitTimeMs = 30000) { + Console.WriteLine("logFilePath{0}", logFilePath); // Initialize fields with constructor arguments _scriptPath = scriptPath; // Path to the PowerShell script to be executed _pwshPath = pwshPath; // Path to the PowerShell executable (pwsh) @@ -63,6 +65,7 @@ public PodePwshMonitor(string scriptPath, string pwshPath, string parameterStrin _disableTermination = disableTermination; // Flag to disable termination of the service _quiet = quiet; // Flag to suppress output for a quieter service _shutdownWaitTimeMs = shutdownWaitTimeMs; // Maximum wait time before forcefully shutting down the process + _logFilePath = logFilePath ?? ".\\PodePwshMonitorService.log"; // Default to local file if none provided // Dynamically generate a unique PipeName for communication _pipeName = $"PodePipe_{Guid.NewGuid()}"; // Generate a unique pipe name to avoid conflicts @@ -101,12 +104,22 @@ public void StartPowerShellProcess() // Start the process _powerShellProcess.Start(); + // Enable raising events to capture process exit and handle cleanup + /* _powerShellProcess.EnableRaisingEvents = true; + _powerShellProcess.Exited += (sender, args) => + { + Log("PowerShell process exited."); + _powerShellProcess.Dispose(); + _powerShellProcess = null; + };*/ + // Log output and error asynchronously _powerShellProcess.OutputDataReceived += (sender, args) => Log(args.Data); _powerShellProcess.ErrorDataReceived += (sender, args) => Log(args.Data); _powerShellProcess.BeginOutputReadLine(); _powerShellProcess.BeginErrorReadLine(); + _lastLogTime = DateTime.Now; Log("PowerShell process started successfully."); } catch (Exception ex) @@ -116,11 +129,15 @@ public void StartPowerShellProcess() } else { - Log("PowerShell process is already running."); + // Log only if more than a minute has passed since the last log + if ((DateTime.Now - _lastLogTime).TotalMinutes >= 1) + { + Log("PowerShell process is already running."); + _lastLogTime = DateTime.Now; + } } } - public void StopPowerShellProcess() { try @@ -181,15 +198,22 @@ public void StopPowerShellProcess() } finally { - // Clean up the named pipe client and process - _powerShellProcess?.Dispose(); - _powerShellProcess = null; - _pipeClient?.Dispose(); + // Set _powerShellProcess to null only if it's still not null + if (_powerShellProcess != null) + { + _powerShellProcess?.Dispose(); + _powerShellProcess = null; + } + // Clean up the pipe client + if (_pipeClient != null) + { + _pipeClient?.Dispose(); + _pipeClient = null; + } Log("PowerShell process and pipe client disposed."); } } - public void RestartPowerShellProcess() { // Simply send the restart message, no need to stop and start again @@ -200,7 +224,6 @@ public void RestartPowerShellProcess() } } - private void SendPipeMessage(string message) { if (_pipeClient == null) @@ -231,7 +254,6 @@ private void SendPipeMessage(string message) } } - private void Log(string data) { if (!string.IsNullOrEmpty(data)) diff --git a/src/PodePwshMonitor/PodePwshWorker.cs b/src/PodePwshMonitor/PodePwshWorker.cs new file mode 100644 index 000000000..393de4791 --- /dev/null +++ b/src/PodePwshMonitor/PodePwshWorker.cs @@ -0,0 +1,67 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Pode.Services +{ + public class PodePwshWorker : BackgroundService + { + private readonly ILogger _logger; + private PodePwshMonitor _pwshMonitor; + + public PodePwshWorker(ILogger logger, IOptions options) + { + _logger = logger; + + // Get options from configuration + var workerOptions = options.Value; + // Print options to console + Console.WriteLine("Worker options: "); + Console.WriteLine(workerOptions.ToString()); + + _pwshMonitor = new PodePwshMonitor( + workerOptions.ScriptPath, + workerOptions.PwshPath, + workerOptions.ParameterString, + workerOptions.LogFilePath, + workerOptions.Quiet, + workerOptions.DisableTermination, + workerOptions.ShutdownWaitTimeMs + ); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("PodePwshWorker running at: {time}", DateTimeOffset.Now); + + while (!stoppingToken.IsCancellationRequested) + { + _pwshMonitor.StartPowerShellProcess(); + await Task.Delay(10000, stoppingToken); // Monitor every 10 seconds + } + } + + public override Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Service is stopping at: {time}", DateTimeOffset.Now); + _pwshMonitor.StopPowerShellProcess(); + return base.StopAsync(stoppingToken); + } + + // Custom RestartAsync method that sends a restart message via pipe + public Task RestartAsync() + { + _logger.LogInformation("Service is restarting at: {time}", DateTimeOffset.Now); + + // Send the 'restart' message using the pipe + _pwshMonitor.RestartPowerShellProcess(); + + _logger.LogInformation("Restart message sent via pipe at: {time}", DateTimeOffset.Now); + return Task.CompletedTask; + } + } +} diff --git a/src/PodePwshMonitor/PodePwshWorkerOptions.cs b/src/PodePwshMonitor/PodePwshWorkerOptions.cs new file mode 100644 index 000000000..7424db84b --- /dev/null +++ b/src/PodePwshMonitor/PodePwshWorkerOptions.cs @@ -0,0 +1,20 @@ +namespace Pode.Services +{ + public class PodePwshWorkerOptions + { + public string ScriptPath { get; set; } + public string PwshPath { get; set; } + public string ParameterString { get; set; } = ""; + public string LogFilePath { get; set; } = ""; + public bool Quiet { get; set; } = true; + public bool DisableTermination { get; set; } = true; + public int ShutdownWaitTimeMs { get; set; } = 30000; + + public override string ToString() + { + return $"ScriptPath: {ScriptPath}, PwshPath: {PwshPath}, ParameterString: {ParameterString}, " + + $"LogFilePath: {LogFilePath}, Quiet: {Quiet}, DisableTermination: {DisableTermination}, " + + $"ShutdownWaitTimeMs: {ShutdownWaitTimeMs}"; + } + } +} diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 7b9fbdbbd..764dfd478 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -67,7 +67,7 @@ function Start-PodeServiceHearthbeat { $PodeContext.Server.Service.PipeName, [System.IO.Pipes.PipeDirection]::InOut, 2, # Max number of allowed concurrent connections - [System.IO.Pipes.PipeTransmissionMode]::Message, + [System.IO.Pipes.PipeTransmissionMode]::Byte, [System.IO.Pipes.PipeOptions]::None ) @@ -115,11 +115,6 @@ function Start-PodeServiceHearthbeat { # Start the runspace that runs the client receiver script block $PodeContext.Server.Service['Runspace'] = Add-PodeRunspace -Type 'Service' -ScriptBlock ($scriptBlock) -PassThru } - else { - # Log when the service is not enabled - Write-PodeServiceLog -Message 'Service is not working' - Write-PodeServiceLog -Message ($PodeService | ConvertTo-Json -Compress) - } } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 new file mode 100644 index 000000000..0c932820d --- /dev/null +++ b/src/Public/Service.ps1 @@ -0,0 +1,169 @@ +<# +.SYNOPSIS + Registers a new Windows service to run a Pode-based PowerShell worker as a service. + +.DESCRIPTION + The `Register-PodeService` function configures and registers a Windows service for running a Pode-based PowerShell worker. + It dynamically sets up the service with the specified parameters, including paths to the script and log files, PowerShell executable, + and service settings. It also generates a `srvsettings.json` file containing the service's configuration. + +.PARAMETER Name + The name of the Windows service to be registered. + +.PARAMETER Description + A brief description of the service. Defaults to "This is a Pode service." + +.PARAMETER DisplayName + The display name of the service, as it will appear in the Windows Services Manager. Defaults to "Pode Service($Name)". + +.PARAMETER StartupType + The startup type of the service (e.g., Automatic, Manual, Disabled). Defaults to 'Automatic'. + +.PARAMETER ParameterString + Any additional parameters to pass to the script when it is run by the service. Defaults to an empty string. + +.PARAMETER Quiet + A boolean value indicating whether to run the service quietly, suppressing logs and output. Defaults to `$true`. + +.PARAMETER DisableTermination + A boolean value indicating whether to disable termination of the service from within the worker process. Defaults to `$true`. + +.PARAMETER ShutdownWaitTimeMs + The maximum amount of time, in milliseconds, to wait for the service to gracefully shut down before forcefully terminating it. Defaults to 30,000 milliseconds. + +.EXAMPLE + Register-PodeService -Name "PodeExampleService" -Description "Example Pode Service" -ParameterString "-Verbose" + + Registers a new Pode-based service called "PodeExampleService" with verbose logging enabled. + +.NOTES + - The function dynamically determines the PowerShell executable path. + - A `srvsettings.json` file is generated in the same directory as the main script, containing the configuration for the Pode service. + - The function checks if a service with the specified name already exists and throws an error if it does. + - The service binary path is set to point to the Pode monitor executable (`PodeMonitor.exe`), which is located in the `Bin` directory relative to the script. +#> +function Register-PodeService { + param( + [Parameter(Mandatory = $true)] + [string]$Name, + + [string]$Description = 'This is a Pode service.', + [string]$DisplayName = "Pode Service($Name)", + [Microsoft.PowerShell.Commands.ServiceStartupType] $StartupType = 'Automatic', + + [string]$ParameterString = '', + [bool]$Quiet = $true, + [bool]$DisableTermination = $true, + [int]$ShutdownWaitTimeMs = 30000 + ) + + if ($MyInvocation.ScriptName) { + $MainScriptPath = Split-Path -Path $MyInvocation.ScriptName -Parent + $MainScriptFileName = Split-Path -Path $MyInvocation.ScriptName -Leaf + } + else { + return $null + } + + # Define script and log file paths + $ScriptPath = Join-Path -Path $MainScriptPath -ChildPath $MainScriptFileName # Example script path + $LogFilePath = Join-Path -Path $MainScriptPath -ChildPath "/logs/$($Name)_svc.log" + + # Obtain the PowerShell path dynamically + $PwshPath = (Get-Process -Id $PID).Path + + # Define the settings file path + $settingsFile = "$MainScriptPath/srvsettings.json" + + $binPath = "$(Split-Path -Parent -Path $PSScriptRoot)/Bin" + + # Check if service already exists + if (Get-Service -Name $Name -ErrorAction SilentlyContinue) { + throw "Windows Service '$Name' already exists." + } + + # JSON content for the service settings + $jsonContent = @{ + PodePwshWorker = @{ + ScriptPath = $ScriptPath + PwshPath = $PwshPath + ParameterString = $ParameterString + LogFilePath = $LogFilePath + Quiet = $Quiet + DisableTermination = $DisableTermination + ShutdownWaitTimeMs = $ShutdownWaitTimeMs + } + } + + # Convert hash table to JSON and save it to the settings file + $jsonContent | ConvertTo-Json | Set-Content -Path $settingsFile -Encoding UTF8 + + # Parameters for New-Service + $params = @{ + Name = $Name + BinaryPathName = "$binPath/PodeMonitor.exe $settingsFile" + DisplayName = $DisplayName + StartupType = $StartupType + Description = $Description + } + try { + return New-Service @params + } + catch { + $_ | Write-PodeErrorLog + } +} + +<# +.SYNOPSIS + Unregisters and removes an existing Pode-based Windows service. + +.DESCRIPTION + The `Unregister-PodeService` function stops and removes an existing Windows service that was previously registered using `Register-PodeService`. + It checks if the service exists and, if running, stops it before removing it from the system. + +.PARAMETER Name + The name of the Windows service to be unregistered and removed. + +.EXAMPLE + Unregister-PodeService -Name "PodeExampleService" + + Unregisters and removes the Pode-based service named "PodeExampleService". + +.NOTES + - This function checks if the service is running before attempting to stop it. + - If the service is not found, it will throw an error. + - You can customize this function to remove any associated files (like configuration files) by uncommenting the relevant section for deleting the settings file. +#> +function Unregister-PodeService { + param( + [Parameter(Mandatory = $true)] + [string]$Name + ) + + # Check if the service exists + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if (-not $service) { + throw ("Service '$Name' does not exist.") + } + + try { + # Check if the service is running before attempting to stop it + if ($service.Status -eq 'Running') { + Stop-Service -Name $Name -Force -ErrorAction Stop + } + + # Remove the service + Remove-Service -Name $Name -ErrorAction Stop + } + catch { + # Handle errors (if needed, you can implement error handling here) + throw $_ # Re-throw the exception for the caller to handle + } + + # Optionally, remove the settings file + # $settingsFile = "$PWD/srvsettings.json" +# if (Test-Path -Path $settingsFile) { + # Remove-Item -Path $settingsFile -Force + #} +} From 2791504be219ffa483b4e80589deb8b3dd4cbc8a Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 16 Oct 2024 19:50:14 -0700 Subject: [PATCH 03/93] update --- pode.build.ps1 | 36 ++++- src/PodePwshMonitor/PodeMonitor.csproj | 10 +- src/PodePwshMonitor/PodePwshMain.cs | 45 ++++--- src/Public/Service.ps1 | 176 +++++++++++++++++++++---- 4 files changed, 216 insertions(+), 51 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index c6d703716..3f288413e 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -164,10 +164,36 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { else { $AssemblyVersion = '' } + foreach ($target in @('win-x64', 'linux-x64', 'osx-x64', 'osx-arm64')) { + dotnet publish --configuration Release $AssemblyVersion --runtime $target --output ../Bin/$target + if (!$?) { + throw "dotnet publish failed for $($target)" + } + } - dotnet publish --configuration Release $AssemblyVersion --output ../Bin/$target - if (!$?) { - throw "dotnet publish failed for $($target)" + # Check if 'lipo' exists + $lipoExists = Get-Command lipo -ErrorAction SilentlyContinue + + if ($lipoExists) { + # Define the paths for the x64 and arm64 binaries and the universal output + $osxX64Path = '../Bin/osx-x64/PodeMonitor' + $osxArm64Path = '../Bin/osx-arm64/PodeMonitor' + $universalPath = '../Bin/osx-universal/PodeMonitor' + # Run 'lipo' to combine x64 and arm64 binaries into a universal binary + $lipoCommand = "lipo -create $osxX64Path $osxArm64Path -output $universalPath" + Write-Host 'Running lipo to create universal binary...' + + # Run the lipo command + try { + Invoke-Expression $lipoCommand + Write-Host "Universal binary created at: $universalPath" + } + catch { + Write-Host "Failed to create universal binary: $_" + } + } + else { + Write-Host "'lipo' not found. Please install 'lipo' to create a universal binary." } } @@ -520,7 +546,7 @@ Task Pack 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','Bin') + $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 @@ -721,7 +747,7 @@ Task CleanLibs { Write-Host "Removing $path contents" Remove-Item -Path $path -Recurse -Force | Out-Null } - + $path = './src/Bin' if (Test-Path -Path $path -PathType Container) { Write-Host "Removing $path contents" diff --git a/src/PodePwshMonitor/PodeMonitor.csproj b/src/PodePwshMonitor/PodeMonitor.csproj index 488deb338..7b0b88a91 100644 --- a/src/PodePwshMonitor/PodeMonitor.csproj +++ b/src/PodePwshMonitor/PodeMonitor.csproj @@ -3,9 +3,10 @@ Exe net8.0 + true true true - win-x64 + win-x64;linux-x64;osx-x64;osx-arm64 @@ -13,7 +14,10 @@ - + + + + - + \ No newline at end of file diff --git a/src/PodePwshMonitor/PodePwshMain.cs b/src/PodePwshMonitor/PodePwshMain.cs index 1353ea8e0..6b62a4b1b 100644 --- a/src/PodePwshMonitor/PodePwshMain.cs +++ b/src/PodePwshMonitor/PodePwshMain.cs @@ -1,7 +1,8 @@ +using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; -using System; + namespace Pode.Services { @@ -11,26 +12,36 @@ public static void Main(string[] args) { var customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Custom config file from args or default - try + var builder = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((context, config) => + { + config.AddJsonFile(customConfigFile, optional: false, reloadOnChange: true); + }) + .ConfigureServices((context, services) => + { + // Bind configuration to PodePwshWorkerOptions + services.Configure(context.Configuration.GetSection("PodePwshWorker")); + + // Add your worker service + services.AddHostedService(); + }); + + // Check if running on Linux and use Systemd + if (OperatingSystem.IsLinux()) + { + builder.UseSystemd(); + } + // Check if running on Windows and use Windows Service + else if (OperatingSystem.IsWindows()) { - Host.CreateDefaultBuilder(args) - .UseWindowsService() // For running as a Windows service - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile(customConfigFile, optional: false, reloadOnChange: true); - }) - .ConfigureServices((context, services) => - { - services.Configure(context.Configuration.GetSection("PodePwshWorker")); - services.AddHostedService(); - }) - .Build() - .Run(); + builder.UseWindowsService(); } - catch (Exception ex) + else if (OperatingSystem.IsMacOS()) { - Console.WriteLine($"An error occurred: {ex.Message}"); + // No specific macOS service manager, it runs under launchd } + + builder.Build().Run(); } } } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 0c932820d..a7dd34d55 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -48,13 +48,33 @@ function Register-PodeService { [string]$Name, [string]$Description = 'This is a Pode service.', + [string]$DisplayName = "Pode Service($Name)", - [Microsoft.PowerShell.Commands.ServiceStartupType] $StartupType = 'Automatic', + + [string] + [validateset('Manual', 'Automatic')] + $StartupType = 'Automatic', + + [string] + $SecurityDescriptorSddl, [string]$ParameterString = '', [bool]$Quiet = $true, [bool]$DisableTermination = $true, - [int]$ShutdownWaitTimeMs = 30000 + [int]$ShutdownWaitTimeMs = 30000, + + [string] + $User = 'podeuser', + [string] + $Group = 'podeuser', + + [switch] + $Start, + [switch] + $SkipUserCreation, + + [pscredential] + $Credential ) if ($MyInvocation.ScriptName) { @@ -62,12 +82,13 @@ function Register-PodeService { $MainScriptFileName = Split-Path -Path $MyInvocation.ScriptName -Leaf } else { - return $null + return $null } # Define script and log file paths $ScriptPath = Join-Path -Path $MainScriptPath -ChildPath $MainScriptFileName # Example script path - $LogFilePath = Join-Path -Path $MainScriptPath -ChildPath "/logs/$($Name)_svc.log" + $LogPath = Join-Path -Path $MainScriptPath -ChildPath '/logs' + $LogFilePath = Join-Path -Path $LogPath -ChildPath "$($Name)_svc.log" # Obtain the PowerShell path dynamically $PwshPath = (Get-Process -Id $PID).Path @@ -75,12 +96,7 @@ function Register-PodeService { # Define the settings file path $settingsFile = "$MainScriptPath/srvsettings.json" - $binPath = "$(Split-Path -Parent -Path $PSScriptRoot)/Bin" - - # Check if service already exists - if (Get-Service -Name $Name -ErrorAction SilentlyContinue) { - throw "Windows Service '$Name' already exists." - } + $binPath = "$(Split-Path -Parent -Path $PSScriptRoot)/Bin" # JSON content for the service settings $jsonContent = @{ @@ -98,19 +114,127 @@ function Register-PodeService { # Convert hash table to JSON and save it to the settings file $jsonContent | ConvertTo-Json | Set-Content -Path $settingsFile -Encoding UTF8 - # Parameters for New-Service - $params = @{ - Name = $Name - BinaryPathName = "$binPath/PodeMonitor.exe $settingsFile" - DisplayName = $DisplayName - StartupType = $StartupType - Description = $Description - } - try { - return New-Service @params - } - catch { - $_ | Write-PodeErrorLog + switch ( [System.Environment]::OSVersion.Platform) { + + [System.PlatformID]::Win32NT { + + # Check if service already exists + if (Get-Service -Name $Name -ErrorAction SilentlyContinue) { + throw "Windows Service '$Name' already exists." + } + + # Parameters for New-Service + $params = @{ + Name = $Name + BinaryPathName = "`"$binPath/PodeMonitor.exe`" `"$settingsFile`"" + DisplayName = $DisplayName + StartupType = $StartupType + Description = $Description + DependsOn = 'NetLogon' + } + if ($Credential) { + $params['Credential'] = $Credential + } + if ($SecurityDescriptorSddl) { + $params['SecurityDescriptorSddl'] = $SecurityDescriptorSddl + } + + try { + $service = New-Service @params + if ($Start.IsPresent) { + # Start the service + Start-Service -InputObject $service + } + } + catch { + $_ | Write-PodeErrorLog + } + } + + [System.PlatformID]::Unix { + @" +[Unit] +Description=$Description +After=network.target + +[Service] +ExecStart=$binPath/linux-x64/PodeMonitor $settingsFile +WorkingDirectory=$MainScriptPath +Restart=always +User=$User +Group=$Group +# Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1 +# Environment=ASPNETCORE_ENVIRONMENT=Production + +[Install] +WantedBy=multi-user.target +"@| Set-Content -Path "/etc/systemd/system/$($Name).service" -Encoding UTF8 + + if (!$SkipUserCreation.IsPresent) { + # Run the id command to check if the user exists + $result = id $User 2>&1 + if ($result -match 'no such user') { + # Create the user + useradd -r -s /bin/false $User + } + } + + # Enable the service + systemctl enable $($Name).service + + if ($Start.IsPresent) { + # Start the service + systemctl start $($Name).service + } + + } + [System.PlatformID]::MacOSX { + $macOsArch = 'osx-arm64' + if ($StartupType -eq 'Automatic') { + $runAtLoad = 'true' + } + else { + $runAtLoad = 'false' + } + @" + + + + + Label + pode.$Name + + ProgramArguments + + $binPath/$macOsArch/PodeMonitor + $settingsFile + + + WorkingDirectory + $MainScriptPath + + RunAtLoad + <$runAtLoad/> + + StandardOutPath + $LogPath/stdout.log + + StandardErrorPath + $LogPath/stderr.log + + KeepAlive + + + +"@| Set-Content -Path "~/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 + + launchctl load /Library/LaunchDaemons/pode.$($Name).plist + if ($Start.IsPresent) { + # Start the service + launchctl start pode.$($Name) + } + } + } } @@ -162,8 +286,8 @@ function Unregister-PodeService { } # Optionally, remove the settings file - # $settingsFile = "$PWD/srvsettings.json" -# if (Test-Path -Path $settingsFile) { - # Remove-Item -Path $settingsFile -Force + # $settingsFile = "$PWD/srvsettings.json" + # if (Test-Path -Path $settingsFile) { + # Remove-Item -Path $settingsFile -Force #} } From d60d1cc2b2e268714b180ae75b4a826982ea7571 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 17 Oct 2024 10:21:14 -0700 Subject: [PATCH 04/93] Update to linux, Mac --- examples/HelloService/HelloService.ps1 | 10 +- examples/HelloService/srvsettings.json | 7 +- pode.build.ps1 | 4 +- src/Pode.psd1 | 5 +- src/PodePwshMonitor/PodeMonitor.csproj | 2 +- src/Private/Service.ps1 | 557 ++++++++++++++++++-- src/Public/Service.ps1 | 685 +++++++++++++++++++------ 7 files changed, 1053 insertions(+), 217 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 8cef7d3ef..8b43e3abc 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -55,25 +55,25 @@ catch { if ($Register.IsPresent) { - Register-PodeService -Name 'HelloService' + Register-PodeService -Name 'HelloService' -Start exit } if ($Unregister.IsPresent) { - Unregister-PodeService -Name 'HelloService' + Unregister-PodeService exit } if ($Start.IsPresent) { - Start-Service -Name 'HelloService' + Start-PodeService exit } if ($Stop.IsPresent) { - Stop-Service -Name 'HelloService' + Stop-PodeService exit } if ($Query.IsPresent) { - Get-Service -Name 'HelloService' + Get-PodeService exit } # Alternatively, you can directly import the Pode module from the system diff --git a/examples/HelloService/srvsettings.json b/examples/HelloService/srvsettings.json index 5c788b9c9..72aec77b4 100644 --- a/examples/HelloService/srvsettings.json +++ b/examples/HelloService/srvsettings.json @@ -1,11 +1,12 @@ { "PodePwshWorker": { - "ParameterString": "", - "Quiet": true, "ScriptPath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", "DisableTermination": true, + "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService_svc.log", + "Quiet": true, "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + "Name": "HelloService", "ShutdownWaitTimeMs": 30000, - "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService_svc.log" + "ParameterString": "" } } diff --git a/pode.build.ps1 b/pode.build.ps1 index 3f288413e..18c8a2731 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -164,7 +164,7 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { else { $AssemblyVersion = '' } - foreach ($target in @('win-x64', 'linux-x64', 'osx-x64', 'osx-arm64')) { + foreach ($target in @('win-x64','win-arm64' ,'linux-x64','linux-arm64', 'osx-x64', 'osx-arm64')) { dotnet publish --configuration Release $AssemblyVersion --runtime $target --output ../Bin/$target if (!$?) { throw "dotnet publish failed for $($target)" @@ -348,7 +348,7 @@ Task PrintChecksum { Task ChocoDeps -If (Test-PodeBuildIsWindows) { if (!(Test-PodeBuildCommand 'choco')) { Set-ExecutionPolicy Bypass -Scope Process -Force - Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + Invoke-Expression (([System.Net.WebClient]::new()).DownloadString('https://chocolatey.org/install.ps1')) } } diff --git a/src/Pode.psd1 b/src/Pode.psd1 index a63533cae..1caa1fdae 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -502,7 +502,10 @@ # service 'Register-PodeService', - 'Unregister-PodeService' + 'Unregister-PodeService', + 'Start-PodeService', + 'Stop-PodeService', + 'Get-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/PodePwshMonitor/PodeMonitor.csproj b/src/PodePwshMonitor/PodeMonitor.csproj index 7b0b88a91..afd537461 100644 --- a/src/PodePwshMonitor/PodeMonitor.csproj +++ b/src/PodePwshMonitor/PodeMonitor.csproj @@ -6,7 +6,7 @@ true true true - win-x64;linux-x64;osx-x64;osx-arm64 + win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 764dfd478..593b8eaf6 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -5,16 +5,16 @@ .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`. -.RETURNS - [Bool] - `$true` if the 'Service' key exists, `$false` if it does not. - .NOTES - This function simply checks the existence of the 'Service' key in `$PodeContext.Server` to determine if the service is enabled. + This is an internal function and may change in future releases of Pode. #> function Test-PodeServiceEnabled { @@ -23,7 +23,6 @@ function Test-PodeServiceEnabled { } -#$global:PodeService=@{DisableTermination=$true;Quiet=$false;Pipename='ssss'} <# .SYNOPSIS Starts the Pode Service Heartbeat using a named pipe for communication with a C# service. @@ -42,14 +41,15 @@ function Test-PodeServiceEnabled { 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 'shutdown' message, the Pode server is stopped. If the pipe receives a 'restart' message, the Pode server is restarted. -.AUTHOR - Your Name + Global variable example: $global:PodeService=@{DisableTermination=$true;Quiet=$false;Pipename='ssss'} #> function Start-PodeServiceHearthbeat { @@ -60,52 +60,54 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { Write-PodeServiceLog -Message "Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" - - try { - # Create a named pipe server stream - $pipeStream = [System.IO.Pipes.NamedPipeServerStream]::new( - $PodeContext.Server.Service.PipeName, - [System.IO.Pipes.PipeDirection]::InOut, - 2, # Max number of allowed concurrent connections - [System.IO.Pipes.PipeTransmissionMode]::Byte, - [System.IO.Pipes.PipeOptions]::None - ) - - Write-PodeServiceLog -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." - $pipeStream.WaitForConnection() # Wait until a client connects - Write-PodeServiceLog -Message "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 - while ($pipeStream.IsConnected) { - $message = $reader.ReadLine() # Read message from the pipe - - if ($message) { - Write-PodeServiceLog -Message "Received message: $message" - - # Process 'shutdown' message - if ($message -eq 'shutdown') { - Write-PodeServiceLog -Message 'Server requested shutdown. Closing client...' - Close-PodeServer # Gracefully stop the Pode server - break # Exit the loop - - # Process 'restart' message - } elseif ($message -eq 'restart') { - Write-PodeServiceLog -Message 'Server requested restart. Restarting client...' - Restart-PodeServer # Restart the Pode server - break # Exit the loop + try { + # Create a named pipe server stream + $pipeStream = [System.IO.Pipes.NamedPipeServerStream]::new( + $PodeContext.Server.Service.PipeName, + [System.IO.Pipes.PipeDirection]::InOut, + 2, # Max number of allowed concurrent connections + [System.IO.Pipes.PipeTransmissionMode]::Byte, + [System.IO.Pipes.PipeOptions]::None + ) + + Write-PodeServiceLog -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." + $pipeStream.WaitForConnection() # Wait until a client connects + Write-PodeServiceLog -Message "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 + while ($pipeStream.IsConnected) { + $message = $reader.ReadLine() # Read message from the pipe + + if ($message) { + Write-PodeServiceLog -Message "Received message: $message" + + # Process 'shutdown' message + if ($message -eq 'shutdown') { + Write-PodeServiceLog -Message 'Server requested shutdown. Closing client...' + Close-PodeServer # Gracefully stop the Pode server + break # Exit the loop + + # Process 'restart' message + } + elseif ($message -eq 'restart') { + Write-PodeServiceLog -Message 'Server requested restart. Restarting client...' + Restart-PodeServer # Restart the Pode server + break # Exit the loop + } } } } - } - catch { - $_ | Write-PodeServiceLog # Log any errors that occur during pipe operation - } - finally { - $pipeStream.Dispose() # Always dispose of the pipe stream when done - } + catch { + $_ | Write-PodeServiceLog # Log any errors that occur during pipe operation + } + finally { + $reader.Dispose() + $pipeStream.Dispose() # Always dispose of the pipe stream when done + } + } # Assign a name to the Pode service @@ -119,6 +121,461 @@ function Start-PodeServiceHearthbeat { + +<# +.SYNOPSIS + Retrieves the service name from the `srvsettings.json` file located at the specified path. + +.DESCRIPTION + The `Get-PodeServiceName` function loads the service configuration from the provided path and retrieves the service name from the `srvsettings.json` file. + If the file does not exist or the name cannot be found in the file, an error is thrown. + +.PARAMETER Path + The directory path where the `srvsettings.json` file is located. + +.EXAMPLE + $serviceName = Get-PodeServiceName -Path "C:\PodeService" + + Retrieves the service name from the `srvsettings.json` file located at "C:\PodeService". + +.NOTES + This is an internal function and may change in future releases of Pode. + The function will throw an error if the settings file does not exist, is malformed, or the service name cannot be determined. +#> +function Get-PodeServiceName { + param( + [Parameter(Mandatory = $true)] + [string] + $Path + ) + + # Define the settings file path + $settingsFile = "$Path/srvsettings.json" + + + if (!(Test-Path -Path $settingsFile -PathType Leaf)) { + throw ($PodeLocale.pathNotExistExceptionMessage -f $settingsFile) + } + # Load the settings from the JSON file + try { + $settings = Get-Content -Path $settingsFile -Raw | ConvertFrom-Json + } + catch { + throw "Failed to load or parse the settings file '$settingsFile'. Error: $_" + } + + # Attempt to retrieve the name from the settings + if ($settings.PodePwshWorker -and $settings.PodePwshWorker.Name) { + return $settings.PodePwshWorker.Name + } + + throw ('Service name could not be determined from {0}' -f $settingsFile) + +} + +<# +.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`). + +.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, + [switch]$Start, + [switch]$Autostart, + [string]$OsArchitecture + ) + + # Check if the service is already registered + if (launchctl list | Select-String "pode.$Name") { + throw 'Service is already registered.' + } + + # Determine whether the service should run at load + $runAtLoad = if ($Autostart.IsPresent) { '' } else { '' } + + # Create the plist content + @" + + + + + Label + pode.$Name + + ProgramArguments + + $BinPath/$OsArchitecture/PodeMonitor + $SettingsFile + + + WorkingDirectory + $BinPath + + RunAtLoad + $runAtLoad + + StandardOutPath + /var/log/pode.$Name.stdout.log + + StandardErrorPath + /var/log/pode.$Name.stderr.log + + KeepAlive + + + +"@ | Set-Content -Path "~/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 + try { + # Load the plist with launchctl + launchctl load ~/Library/LaunchAgents/pode.$($Name).plist + + # Verify the service is now registered + if (-not (launchctl list | Select-String "pode.$Name")) { + throw 'Service failed to register.' + } + } + catch { + $_ | Write-PodeErrorLog + throw $_ # Rethrow the error after logging + } + + # Optionally start the service + if ($Start.IsPresent) { + Start-PodeService + } + + 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 Start + A switch indicating whether to start the service immediately after it is registered. + +.PARAMETER SkipUserCreation + A switch to skip the creation of the user if it does not exist. + +.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, + + [switch] + $SkipUserCreation, + + [string] + $OsArchitecture + ) + + # Check if the service is already registered + if (systemctl status "$Name.service" -ErrorAction SilentlyContinue) { + throw 'Service is already registered.' + } + + # Create the service file + @" +[Unit] +Description=$Description +After=network.target + +[Service] +ExecStart=$BinPath/linux-$OsArchitecture/PodeMonitor $SettingsFile +WorkingDirectory=$BinPath +Restart=always +User=$User +Group=$Group +# Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1 +# Environment=ASPNETCORE_ENVIRONMENT=Production + +[Install] +WantedBy=multi-user.target +"@ | Set-Content -Path "/etc/systemd/system/$($Name).service" -Encoding UTF8 + + # Create user if needed + if (!$SkipUserCreation.IsPresent) { + # Run the id command to check if the user exists + id $User 2>&1 + if ($LASTEXITCODE -ne 0) { + # Create the user if it doesn't exist + useradd -r -s /bin/false $User + } + } + + # Enable the service and check if it fails + try { + systemctl enable "$Name.service" + if ($LASTEXITCODE -ne 0) { + throw 'Service failed to register.' + } + } + catch { + $_ | Write-PodeErrorLog + throw $_ # Rethrow the error after logging + } + + # Optionally start the service + if ($Start.IsPresent) { + Start-PodeService + } + + return $true +} + + + +<# +.SYNOPSIS + Registers a new Windows service to run a Pode-based PowerShell worker. + +.DESCRIPTION + The `Register-PodeWindowsService` 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 Start + A switch to start the service immediately after it is registered. + +.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-PodeWindowsService -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-PodeWindowsService -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-PodeWindowsService { + param( + [string] + $Name, + + [string] + $Description, + + [string] + $DisplayName, + + [string] + $StartupType, + + [string] + $BinPath, + + [string] + $SettingsFile, + + [pscredential] + $Credential, + + [string] + $SecurityDescriptorSddl, + + [switch] + $Start, + + [string] + $OsArchitecture + ) + + + # Check if service already exists + if (Get-Service -Name $Name -ErrorAction SilentlyContinue) { + throw 'Service is already registered.' + } + + # Parameters for New-Service + $params = @{ + Name = $Name + BinaryPathName = "`"$BinPath\win-$OsArchitecture\PodeMonitor.exe`" `"$SettingsFile`"" + DisplayName = $DisplayName + StartupType = $StartupType + Description = $Description + #DependsOn = 'NetLogon' + } + if ($Credential) { + $params['Credential'] = $Credential + } + if ($SecurityDescriptorSddl) { + $params['SecurityDescriptorSddl'] = $SecurityDescriptorSddl + } + + try { + $sv = New-Service @params -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + if (!$sv) { + throw 'Service failed to register.' + } + } + catch { + $_ | Write-PodeErrorLog + throw $_ # Rethrow the error after logging + } + if ($Start.IsPresent) { + # Start the service + Start-PodeService + } + return $true +} + + function Write-PodeServiceLog { [CmdletBinding(DefaultParameterSetName = 'Message')] param( diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index a7dd34d55..fc7c85e6c 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -1,23 +1,27 @@ <# .SYNOPSIS - Registers a new Windows service to run a Pode-based PowerShell worker as a service. + Registers a new service to run a Pode-based PowerShell worker as a service on multiple platforms. .DESCRIPTION - The `Register-PodeService` function configures and registers a Windows service for running a Pode-based PowerShell worker. - It dynamically sets up the service with the specified parameters, including paths to the script and log files, PowerShell executable, - and service settings. It also generates a `srvsettings.json` file containing the service's configuration. + The `Register-PodeService` function configures and registers a service for running a Pode-based PowerShell worker on Windows, Linux, or macOS. + It dynamically sets up the service with the specified parameters, including paths to the script, log files, PowerShell executable, + and service settings. It also generates a `srvsettings.json` file containing the service's configuration and registers the service + using platform-specific methods. .PARAMETER Name - The name of the Windows service to be registered. + The name of the service to be registered. .PARAMETER Description A brief description of the service. Defaults to "This is a Pode service." .PARAMETER DisplayName - The display name of the service, as it will appear in the Windows Services Manager. Defaults to "Pode Service($Name)". + The display name of the service, as it will appear in the Windows Services Manager (Windows only). Defaults to "Pode Service($Name)". .PARAMETER StartupType - The startup type of the service (e.g., Automatic, Manual, Disabled). Defaults to 'Automatic'. + The startup type of the service (e.g., Automatic, Manual). Defaults to 'Automatic'. + +.PARAMETER SecurityDescriptorSddl + The security descriptor in SDDL format for the service (Windows only). .PARAMETER ParameterString Any additional parameters to pass to the script when it is run by the service. Defaults to an empty string. @@ -31,25 +35,44 @@ .PARAMETER ShutdownWaitTimeMs The maximum amount of time, in milliseconds, to wait for the service to gracefully shut down before forcefully terminating it. Defaults to 30,000 milliseconds. +.PARAMETER User + The user under which the service should run. Defaults to `podeuser`. + +.PARAMETER Group + The group under which the service should run (Linux only). Defaults to `podeuser`. + +.PARAMETER Start + A switch to start the service immediately after it is registered. + +.PARAMETER SkipUserCreation + A switch to skip the user creation process (Linux only). + +.PARAMETER Credential + A `PSCredential` object specifying the credentials for the account under which the Windows service will run. + .EXAMPLE Register-PodeService -Name "PodeExampleService" -Description "Example Pode Service" -ParameterString "-Verbose" Registers a new Pode-based service called "PodeExampleService" with verbose logging enabled. .NOTES - - The function dynamically determines the PowerShell executable path. + - This function is cross-platform and handles service registration on Windows, Linux, and macOS. - A `srvsettings.json` file is generated in the same directory as the main script, containing the configuration for the Pode service. - - The function checks if a service with the specified name already exists and throws an error if it does. - - The service binary path is set to point to the Pode monitor executable (`PodeMonitor.exe`), which is located in the `Bin` directory relative to the script. + - The function checks if a service with the specified name already exists on the respective platform and throws an error if it does. + - For Windows, the service binary path points to the Pode monitor executable (`PodeMonitor.exe`), which is located in the `Bin` directory relative to the script. + - This function dynamically determines the PowerShell executable path and system architecture. #> function Register-PodeService { param( [Parameter(Mandatory = $true)] - [string]$Name, + [string] + $Name, - [string]$Description = 'This is a Pode service.', + [string] + $Description = 'This is a Pode service.', - [string]$DisplayName = "Pode Service($Name)", + [string] + $DisplayName = "Pode Service($Name)", [string] [validateset('Manual', 'Automatic')] @@ -58,18 +81,27 @@ function Register-PodeService { [string] $SecurityDescriptorSddl, - [string]$ParameterString = '', - [bool]$Quiet = $true, - [bool]$DisableTermination = $true, - [int]$ShutdownWaitTimeMs = 30000, + [string] + $ParameterString = '', + + [bool] + $Quiet = $true, + + [bool] + $DisableTermination = $true, + + [int] + $ShutdownWaitTimeMs = 30000, [string] $User = 'podeuser', + [string] $Group = 'podeuser', [switch] $Start, + [switch] $SkipUserCreation, @@ -78,25 +110,31 @@ function Register-PodeService { ) if ($MyInvocation.ScriptName) { - $MainScriptPath = Split-Path -Path $MyInvocation.ScriptName -Parent - $MainScriptFileName = Split-Path -Path $MyInvocation.ScriptName -Leaf + $ScriptPath = $MyInvocation.ScriptName + $MainScriptPath = Split-Path -Path $ScriptPath -Parent + # $MainScriptFileName = Split-Path -Path $ScriptPath -Leaf } else { return $null } # Define script and log file paths - $ScriptPath = Join-Path -Path $MainScriptPath -ChildPath $MainScriptFileName # Example script path + # $ScriptPath = Join-Path -Path $MainScriptPath -ChildPath $MainScriptFileName # Example script path $LogPath = Join-Path -Path $MainScriptPath -ChildPath '/logs' $LogFilePath = Join-Path -Path $LogPath -ChildPath "$($Name)_svc.log" + # Ensure log directory exists + if (-not (Test-Path $LogPath)) { + New-Item -Path $LogPath -ItemType Directory -Force + } + # Obtain the PowerShell path dynamically $PwshPath = (Get-Process -Id $PID).Path # Define the settings file path - $settingsFile = "$MainScriptPath/srvsettings.json" + $settingsFile = Join-Path -Path $MainScriptPath -ChildPath 'srvsettings.json' - $binPath = "$(Split-Path -Parent -Path $PSScriptRoot)/Bin" + $binPath = Join-Path -path (Split-Path -Parent -Path $PSScriptRoot) -ChildPath 'Bin' # JSON content for the service settings $jsonContent = @{ @@ -108,186 +146,523 @@ function Register-PodeService { Quiet = $Quiet DisableTermination = $DisableTermination ShutdownWaitTimeMs = $ShutdownWaitTimeMs + Name = $Name } } # Convert hash table to JSON and save it to the settings file $jsonContent | ConvertTo-Json | Set-Content -Path $settingsFile -Encoding UTF8 - switch ( [System.Environment]::OSVersion.Platform) { - - [System.PlatformID]::Win32NT { + $osArchitecture = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() + + # Call the appropriate platform-specific function + switch ([System.Environment]::OSVersion.Platform) { + Win32NT { + $param = @{ + Name = $Name + Description = $Description + DisplayName = $DisplayName + StartupType = $StartupType + BinPath = $binPath + SettingsFile = $settingsFile + Credential = $Credential + SecurityDescriptorSddl = $SecurityDescriptorSddl + Start = $Start + OsArchitecture = $osArchitecture + } + Register-PodeWindowsService @param + } - # Check if service already exists - if (Get-Service -Name $Name -ErrorAction SilentlyContinue) { - throw "Windows Service '$Name' already exists." + Unix { + $param = @{ + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + Group = $Group + Start = $Start + SkipUserCreation = $SkipUserCreation + OsArchitecture = $osArchitecture } + Register-PodeLinuxService @param + } - # Parameters for New-Service - $params = @{ + MacOSX { + $param = @{ Name = $Name - BinaryPathName = "`"$binPath/PodeMonitor.exe`" `"$settingsFile`"" - DisplayName = $DisplayName - StartupType = $StartupType Description = $Description - DependsOn = 'NetLogon' - } - if ($Credential) { - $params['Credential'] = $Credential - } - if ($SecurityDescriptorSddl) { - $params['SecurityDescriptorSddl'] = $SecurityDescriptorSddl + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + Start = $Start + OsArchitecture = $osArchitecture } - try { - $service = New-Service @params - if ($Start.IsPresent) { - # Start the service - Start-Service -InputObject $service + + Register-PodeMacService @param + } + } +} + + +<# +.SYNOPSIS + Starts a Pode-based service across different platforms (Windows, Linux, and macOS). + +.DESCRIPTION + The `Start-PodeService` function checks if a Pode-based service is already running, and if not, it starts the service. + It works on Windows, Linux (systemd), and macOS (launchctl), handling platform-specific service commands to start the service. + If the service is not registered, it will throw an error. + +.PARAMETER None + No parameters are required for this function. + +.EXAMPLE + Start-PodeService + + Starts the Pode-based service if it is not currently running. + +.NOTES + - The function retrieves the service name from the `srvsettings.json` file located in the script directory. + - On Windows, it uses `Get-Service` and `Start-Service` to manage the service. + - On Linux, it uses `systemctl` to manage the service. + - On macOS, it uses `launchctl` to manage the service. + - If the service is already running, no action is taken. + - If the service is not registered, the function throws an error. +#> +function Start-PodeService { + try { + # Get the service name from the settings file + $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + + switch ([System.Environment]::OSVersion.Platform) { + Win32NT { + + # Get the Windows service + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is already running + if ($service.Status -ne 'Running') { + Start-Service -Name $name -ErrorAction Stop + # Log service started successfully + # Write-PodeServiceLog -Message "Service '$name' started successfully." + } + else { + # Log service is already running + # Write-PodeServiceLog -Message "Service '$name' is already running." + } + } + else { + throw "Service '$name' is not registered." } } - catch { - $_ | Write-PodeErrorLog + + Unix { + # Check if the service exists + if (systemctl status "$name.service" -q) { + # Check if the service is already running + $status = systemctl is-active "$name.service" + if ($status -ne 'active') { + systemctl start "$name.service" + # Log service started successfully + # Write-PodeServiceLog -Message "Service '$name' started successfully." + } + else { + # Log service is already running + # Write-PodeServiceLog -Message "Service '$name' is already running." + } + } + else { + throw "Service '$name' is not registered." + } } - } - [System.PlatformID]::Unix { - @" -[Unit] -Description=$Description -After=network.target - -[Service] -ExecStart=$binPath/linux-x64/PodeMonitor $settingsFile -WorkingDirectory=$MainScriptPath -Restart=always -User=$User -Group=$Group -# Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1 -# Environment=ASPNETCORE_ENVIRONMENT=Production - -[Install] -WantedBy=multi-user.target -"@| Set-Content -Path "/etc/systemd/system/$($Name).service" -Encoding UTF8 - - if (!$SkipUserCreation.IsPresent) { - # Run the id command to check if the user exists - $result = id $User 2>&1 - if ($result -match 'no such user') { - # Create the user - useradd -r -s /bin/false $User + MacOSX { + # Check if the service exists in launchctl + if (launchctl list | Select-String "pode.$name") { + # Check if the service is already running + if (-not (launchctl list "pode.$name" | Select-String "pode.$name")) { + launchctl start "pode.$name" + # Log service started successfully + # Write-PodeServiceLog -Message "Service '$name' started successfully." + } + else { + # Log service is already running + # Write-PodeServiceLog -Message "Service '$name' is already running." + } + } + else { + throw "Service '$name' is not registered." } } + } + } + catch { + $_ | Write-PodeErrorLog + return $false + } + return $true +} - # Enable the service - systemctl enable $($Name).service +<# +.SYNOPSIS + Stops a Pode-based service across different platforms (Windows, Linux, and macOS). - if ($Start.IsPresent) { - # Start the service - systemctl start $($Name).service - } +.DESCRIPTION + The `Stop-PodeService` function stops a Pode-based service by checking if it is currently running. + If the service is running, it will attempt to stop the service gracefully. + The function works on Windows, Linux (systemd), and macOS (launchctl). - } - [System.PlatformID]::MacOSX { - $macOsArch = 'osx-arm64' - if ($StartupType -eq 'Automatic') { - $runAtLoad = 'true' +.PARAMETER None + No parameters are required for this function. + +.EXAMPLE + Stop-PodeService + + Stops the Pode-based service if it is currently running. If the service is not running, no action is taken. + +.NOTES + - The function retrieves the service name from the `srvsettings.json` file located in the script directory. + - On Windows, it uses `Get-Service` and `Stop-Service`. + - On Linux, it uses `systemctl` to stop the service. + - On macOS, it uses `launchctl` to stop the service. + - If the service is not registered, the function throws an error. +#> +function Stop-PodeService { + try { + # Get the service name from the settings file + $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + + switch ([System.Environment]::OSVersion.Platform) { + Win32NT { + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is running + if ($service.Status -eq 'Running') { + Stop-Service -Name $name -ErrorAction Stop -WarningAction SilentlyContinue + # Write-PodeServiceLog -Message "Service '$name' stopped successfully." + } + else { + # Write-PodeServiceLog -Message "Service '$name' is not running." + } + } + else { + throw "Service '$name' is not registered." + } } - else { - $runAtLoad = 'false' + + Unix { + # Check if the service exists + if (systemctl status "$name.service" -q) { + $status = systemctl is-active "$name.service" + if ($status -eq 'active') { + systemctl stop "$name.service" + # Write-PodeServiceLog -Message "Service '$name' stopped successfully." + } + else { + # Write-PodeServiceLog -Message "Service '$name' is not running." + } + } + else { + throw "Service '$name' is not registered." + } } - @" - - - - - Label - pode.$Name - - ProgramArguments - - $binPath/$macOsArch/PodeMonitor - $settingsFile - - - WorkingDirectory - $MainScriptPath - - RunAtLoad - <$runAtLoad/> - - StandardOutPath - $LogPath/stdout.log - - StandardErrorPath - $LogPath/stderr.log - - KeepAlive - - - -"@| Set-Content -Path "~/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 - - launchctl load /Library/LaunchDaemons/pode.$($Name).plist - if ($Start.IsPresent) { - # Start the service - launchctl start pode.$($Name) + + MacOSX { + # Check if the service exists in launchctl + if (launchctl list | Select-String "pode.$name") { + # Stop the service if running + if (launchctl list "pode.$name" | Select-String "pode.$name") { + launchctl stop "pode.$name" + # Write-PodeServiceLog -Message "Service '$name' stopped successfully." + } + else { + # Write-PodeServiceLog -Message "Service '$name' is not running." + } + } + else { + throw "Service '$name' is not registered." + } } } - } + catch { + $_ | Write-PodeErrorLog + return $false + } + return $true } <# .SYNOPSIS - Unregisters and removes an existing Pode-based Windows service. + Unregisters a Pode-based service across different platforms (Windows, Linux, and macOS). .DESCRIPTION - The `Unregister-PodeService` function stops and removes an existing Windows service that was previously registered using `Register-PodeService`. - It checks if the service exists and, if running, stops it before removing it from the system. + 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 Name - The name of the Windows service to be unregistered and removed. +.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. .EXAMPLE - Unregister-PodeService -Name "PodeExampleService" + Unregister-PodeService -Force + + Unregisters the Pode-based service, forcefully stopping it if it is currently running. - Unregisters and removes the Pode-based service named "PodeExampleService". +.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 - - This function checks if the service is running before attempting to stop it. - - If the service is not found, it will throw an error. - - You can customize this function to remove any associated files (like configuration files) by uncommenting the relevant section for deleting the settings file. + - 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 ) - # Check if the service exists - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if (-not $service) { - throw ("Service '$Name' does not exist.") - } + # Get the service name from the settings file + $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - try { - # Check if the service is running before attempting to stop it - if ($service.Status -eq 'Running') { - Stop-Service -Name $Name -Force -ErrorAction Stop + switch ([System.Environment]::OSVersion.Platform) { + Win32NT { + # Check if the service exists + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if (-not $service) { + throw "Service '$name' is not registered." + } + + try { + # Check if the service is running before attempting to stop it + if ($service.Status -eq 'Running') { + if ($Force.IsPresent) { + Stop-Service -Name $name -Force -ErrorAction Stop + # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + } + else { + throw "Service '$name' is running. Use the -Force parameter to forcefully stop." + } + } + + # Remove the service + Remove-Service -Name $name -ErrorAction Stop + # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." + return $true + } + catch { + $_ | Write-PodeErrorLog + return $false + } } - # Remove the service - Remove-Service -Name $Name -ErrorAction Stop - } - catch { - # Handle errors (if needed, you can implement error handling here) - throw $_ # Re-throw the exception for the caller to handle + Unix { + try { + # Check if the service exists + if (systemctl status "$name.service" -q) { + # Check if the service is running + $status = systemctl is-active "$name.service" + if ($status -eq 'active') { + if ($Force.IsPresent) { + systemctl stop "$name.service" + # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + } + else { + throw "Service '$name' is running. Use the -Force parameter to forcefully stop." + } + } + systemctl disable "$name.service" + Remove-Item "/etc/systemd/system/$name.service" + # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." + } + else { + throw "Service '$name' is not registered." + } + return $true + } + catch { + $_ | Write-PodeErrorLog + return $false + } + } + + MacOSX { + try { + # Check if the service exists + if (launchctl list | Select-String "pode.$name") { + # Check if the service is running + if (launchctl list "pode.$name" | Select-String "pode.$name") { + if ($Force.IsPresent) { + launchctl stop "pode.$name" + # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + } + else { + throw "Service '$name' is running. Use the -Force parameter to forcefully stop." + } + } + launchctl unload "/Library/LaunchDaemons/pode.$name.plist" + Remove-Item "~/Library/LaunchAgents/pode.$name.plist" + # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." + } + else { + throw "Service '$name' is not registered." + } + return $true + } + catch { + $_ | Write-PodeErrorLog + return $false + } + } } +} + + +<# +.SYNOPSIS + Retrieves the status of a Pode service across different platforms (Windows, Linux, and macOS). - # Optionally, remove the settings file - # $settingsFile = "$PWD/srvsettings.json" - # if (Test-Path -Path $settingsFile) { - # Remove-Item -Path $settingsFile -Force - #} +.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 None + This function does not accept any parameters directly, but it relies on the service name from the configuration file + (`srvsettings.json`) located in the script's directory. + +.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 { + + $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + + switch ([System.Environment]::OSVersion.Platform) { + Win32NT { + # Check if the service exists on Windows + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if ($service) { + switch ($service.Status) { + 'Running' { $status = 'Running' } + 'Stopped' { $status = 'Stopped' } + 'Paused' { $status = 'Paused' } + 'StartPending' { $status = 'Starting' } + 'StopPending' { $status = 'Stopping' } + 'PausePending' { $status = 'Pausing' } + 'ContinuePending' { $status = 'Resuming' } + default { $status = 'Unknown' } + } + return @{ + Name = $name + Status = $status + } + } + else { + Write-PodeErrorLog -Message "Service '$name' not found on Windows." + return $null + } + } + + Unix { + try { + # Check if the service exists on Linux (systemd) + $output = systemctl is-active "$name.service" 2>&1 + if ($LASTEXITCODE -eq 0) { + if ($output -match 'active') { + $status = 'Running' + } + elseif ($output -match 'inactive \(dead\)') { + $status = 'Stopped' + } + elseif ($output -match 'activating') { + $status = 'Starting' + } + elseif ($output -match 'deactivating') { + $status = 'Stopping' + } + else { + $status = 'Unknown' + } + return @{ + Name = $name + Status = $status + } + } + else { + return @{ + Name = $name + Status = 'Stopped' + } + } + } + catch { + $_ | Write-PodeErrorLog + return $null + } + } + + MacOSX { + try { + # Check if the service exists on macOS (launchctl) + $serviceList = launchctl list | Select-String "pode.$name" + if ($serviceList) { + $status = launchctl list "pode.$name" 2>&1 + if ($status -match 'PID = (\d+)') { + return @{ + Name = $name + Status = 'Running' + } + } + else { + return @{ + Name = $name + Status = 'Stopped' + } + } + } + else { + Write-PodeErrorLog -Message "Service 'pode.$name' not found on macOS." + return $null + } + } + catch { + $_ | Write-PodeErrorLog + return $null + } + } + } } From 3c3a5cddcf30c8087e3c467fda57d242dbe6e433 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 17 Oct 2024 11:55:45 -0700 Subject: [PATCH 05/93] fix Mac detection --- src/Public/Service.ps1 | 512 ++++++++++++++++++++--------------------- 1 file changed, 249 insertions(+), 263 deletions(-) diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index fc7c85e6c..8a5d44cce 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -156,52 +156,48 @@ function Register-PodeService { $osArchitecture = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() # Call the appropriate platform-specific function - switch ([System.Environment]::OSVersion.Platform) { - Win32NT { - $param = @{ - Name = $Name - Description = $Description - DisplayName = $DisplayName - StartupType = $StartupType - BinPath = $binPath - SettingsFile = $settingsFile - Credential = $Credential - SecurityDescriptorSddl = $SecurityDescriptorSddl - Start = $Start - OsArchitecture = $osArchitecture - } - Register-PodeWindowsService @param + if ($IsWindows) { + $param = @{ + Name = $Name + Description = $Description + DisplayName = $DisplayName + StartupType = $StartupType + BinPath = $binPath + SettingsFile = $settingsFile + Credential = $Credential + SecurityDescriptorSddl = $SecurityDescriptorSddl + Start = $Start + OsArchitecture = $osArchitecture } - - Unix { - $param = @{ - Name = $Name - Description = $Description - BinPath = $binPath - SettingsFile = $settingsFile - User = $User - Group = $Group - Start = $Start - SkipUserCreation = $SkipUserCreation - OsArchitecture = $osArchitecture - } - Register-PodeLinuxService @param + Register-PodeWindowsService @param + } + elseif ($IsLinux) { + $param = @{ + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + Group = $Group + Start = $Start + SkipUserCreation = $SkipUserCreation + OsArchitecture = $osArchitecture + } + Register-PodeLinuxService @param + } + elseif ($IsMacOS) { + $param = @{ + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + Start = $Start + OsArchitecture = $osArchitecture } - - MacOSX { - $param = @{ - Name = $Name - Description = $Description - BinPath = $binPath - SettingsFile = $settingsFile - User = $User - Start = $Start - OsArchitecture = $osArchitecture - } - Register-PodeMacService @param - } + Register-PodeMacService @param } } @@ -236,66 +232,63 @@ function Start-PodeService { # Get the service name from the settings file $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - switch ([System.Environment]::OSVersion.Platform) { - Win32NT { - - # Get the Windows service - $service = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($service) { - # Check if the service is already running - if ($service.Status -ne 'Running') { - Start-Service -Name $name -ErrorAction Stop - # Log service started successfully - # Write-PodeServiceLog -Message "Service '$name' started successfully." - } - else { - # Log service is already running - # Write-PodeServiceLog -Message "Service '$name' is already running." - } + if ($IsWindows) { + # Get the Windows service + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is already running + if ($service.Status -ne 'Running') { + Start-Service -Name $name -ErrorAction Stop + # Log service started successfully + # Write-PodeServiceLog -Message "Service '$name' started successfully." } else { - throw "Service '$name' is not registered." + # Log service is already running + # Write-PodeServiceLog -Message "Service '$name' is already running." } } + else { + throw "Service '$name' is not registered." + } + } - Unix { - # Check if the service exists - if (systemctl status "$name.service" -q) { - # Check if the service is already running - $status = systemctl is-active "$name.service" - if ($status -ne 'active') { - systemctl start "$name.service" - # Log service started successfully - # Write-PodeServiceLog -Message "Service '$name' started successfully." - } - else { - # Log service is already running - # Write-PodeServiceLog -Message "Service '$name' is already running." - } + elseif ($IsLinux) { + # Check if the service exists + if (systemctl status "$name.service" -q) { + # Check if the service is already running + $status = systemctl is-active "$name.service" + if ($status -ne 'active') { + systemctl start "$name.service" + # Log service started successfully + # Write-PodeServiceLog -Message "Service '$name' started successfully." } else { - throw "Service '$name' is not registered." + # Log service is already running + # Write-PodeServiceLog -Message "Service '$name' is already running." } } + else { + throw "Service '$name' is not registered." + } + } - MacOSX { - # Check if the service exists in launchctl - if (launchctl list | Select-String "pode.$name") { - # Check if the service is already running - if (-not (launchctl list "pode.$name" | Select-String "pode.$name")) { - launchctl start "pode.$name" - # Log service started successfully - # Write-PodeServiceLog -Message "Service '$name' started successfully." - } - else { - # Log service is already running - # Write-PodeServiceLog -Message "Service '$name' is already running." - } + elseif ($IsMacOS) { + # Check if the service exists in launchctl + if (launchctl list | Select-String "pode.$name") { + # Check if the service is already running + if (-not (launchctl list "pode.$name" | Select-String "pode.$name")) { + launchctl start "pode.$name" + # Log service started successfully + # Write-PodeServiceLog -Message "Service '$name' started successfully." } else { - throw "Service '$name' is not registered." + # Log service is already running + # Write-PodeServiceLog -Message "Service '$name' is already running." } } + else { + throw "Service '$name' is not registered." + } } } catch { @@ -334,57 +327,54 @@ function Stop-PodeService { # Get the service name from the settings file $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - switch ([System.Environment]::OSVersion.Platform) { - Win32NT { - $service = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($service) { - # Check if the service is running - if ($service.Status -eq 'Running') { - Stop-Service -Name $name -ErrorAction Stop -WarningAction SilentlyContinue - # Write-PodeServiceLog -Message "Service '$name' stopped successfully." - } - else { - # Write-PodeServiceLog -Message "Service '$name' is not running." - } + if ($IsWindows) { + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is running + if ($service.Status -eq 'Running') { + Stop-Service -Name $name -ErrorAction Stop -WarningAction SilentlyContinue + # Write-PodeServiceLog -Message "Service '$name' stopped successfully." } else { - throw "Service '$name' is not registered." + # Write-PodeServiceLog -Message "Service '$name' is not running." } } - - Unix { - # Check if the service exists - if (systemctl status "$name.service" -q) { - $status = systemctl is-active "$name.service" - if ($status -eq 'active') { - systemctl stop "$name.service" - # Write-PodeServiceLog -Message "Service '$name' stopped successfully." - } - else { - # Write-PodeServiceLog -Message "Service '$name' is not running." - } + else { + throw "Service '$name' is not registered." + } + } + elseif ($IsLinux) { + # Check if the service exists + if (systemctl status "$name.service" -q) { + $status = systemctl is-active "$name.service" + if ($status -eq 'active') { + systemctl stop "$name.service" + # Write-PodeServiceLog -Message "Service '$name' stopped successfully." } else { - throw "Service '$name' is not registered." + # Write-PodeServiceLog -Message "Service '$name' is not running." } } + else { + throw "Service '$name' is not registered." + } + } - MacOSX { - # Check if the service exists in launchctl - if (launchctl list | Select-String "pode.$name") { - # Stop the service if running - if (launchctl list "pode.$name" | Select-String "pode.$name") { - launchctl stop "pode.$name" - # Write-PodeServiceLog -Message "Service '$name' stopped successfully." - } - else { - # Write-PodeServiceLog -Message "Service '$name' is not running." - } + elseif ($IsMacOS) { + # Check if the service exists in launchctl + if (launchctl list | Select-String "pode.$name") { + # Stop the service if running + if (launchctl list "pode.$name" | Select-String "pode.$name") { + launchctl stop "pode.$name" + # Write-PodeServiceLog -Message "Service '$name' stopped successfully." } else { - throw "Service '$name' is not registered." + # Write-PodeServiceLog -Message "Service '$name' is not running." } } + else { + throw "Service '$name' is not registered." + } } } catch { @@ -432,94 +422,92 @@ function Unregister-PodeService { # Get the service name from the settings file $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - switch ([System.Environment]::OSVersion.Platform) { - Win32NT { - # Check if the service exists - $service = Get-Service -Name $name -ErrorAction SilentlyContinue - if (-not $service) { - throw "Service '$name' is not registered." + if ($IsWindows) { + # Check if the service exists + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if (-not $service) { + throw "Service '$name' is not registered." + } + + try { + # Check if the service is running before attempting to stop it + if ($service.Status -eq 'Running') { + if ($Force.IsPresent) { + Stop-Service -Name $name -Force -ErrorAction Stop + # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + } + else { + throw "Service '$name' is running. Use the -Force parameter to forcefully stop." + } } - try { - # Check if the service is running before attempting to stop it - if ($service.Status -eq 'Running') { + # Remove the service + Remove-Service -Name $name -ErrorAction Stop + # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." + return $true + } + catch { + $_ | Write-PodeErrorLog + return $false + } + } + + elseif ($IsLinux) { + try { + # Check if the service exists + if (systemctl status "$name.service" -q) { + # Check if the service is running + $status = systemctl is-active "$name.service" + if ($status -eq 'active') { if ($Force.IsPresent) { - Stop-Service -Name $name -Force -ErrorAction Stop + systemctl stop "$name.service" # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." } else { throw "Service '$name' is running. Use the -Force parameter to forcefully stop." } } - - # Remove the service - Remove-Service -Name $name -ErrorAction Stop + systemctl disable "$name.service" + Remove-Item "/etc/systemd/system/$name.service" # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." - return $true } - catch { - $_ | Write-PodeErrorLog - return $false + else { + throw "Service '$name' is not registered." } + return $true } - - Unix { - try { - # Check if the service exists - if (systemctl status "$name.service" -q) { - # Check if the service is running - $status = systemctl is-active "$name.service" - if ($status -eq 'active') { - if ($Force.IsPresent) { - systemctl stop "$name.service" - # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." - } - else { - throw "Service '$name' is running. Use the -Force parameter to forcefully stop." - } - } - systemctl disable "$name.service" - Remove-Item "/etc/systemd/system/$name.service" - # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." - } - else { - throw "Service '$name' is not registered." - } - return $true - } - catch { - $_ | Write-PodeErrorLog - return $false - } + catch { + $_ | Write-PodeErrorLog + return $false } + } - MacOSX { - try { - # Check if the service exists - if (launchctl list | Select-String "pode.$name") { - # Check if the service is running - if (launchctl list "pode.$name" | Select-String "pode.$name") { - if ($Force.IsPresent) { - launchctl stop "pode.$name" - # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." - } - else { - throw "Service '$name' is running. Use the -Force parameter to forcefully stop." - } + elseif ($IsMacOS) { + try { + # Check if the service exists + if (launchctl list | Select-String "pode.$name") { + # Check if the service is running + if (launchctl list "pode.$name" | Select-String "pode.$name") { + if ($Force.IsPresent) { + launchctl stop "pode.$name" + # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + } + else { + throw "Service '$name' is running. Use the -Force parameter to forcefully stop." } - launchctl unload "/Library/LaunchDaemons/pode.$name.plist" - Remove-Item "~/Library/LaunchAgents/pode.$name.plist" - # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." - } - else { - throw "Service '$name' is not registered." } - return $true + launchctl unload "/Library/LaunchDaemons/pode.$name.plist" + Remove-Item "~/Library/LaunchAgents/pode.$name.plist" + # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." } - catch { - $_ | Write-PodeErrorLog - return $false + else { + throw "Service '$name' is not registered." } + return $true + } + catch { + $_ | Write-PodeErrorLog + return $false } } } @@ -571,20 +559,50 @@ function Get-PodeService { $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - switch ([System.Environment]::OSVersion.Platform) { - Win32NT { - # Check if the service exists on Windows - $service = Get-Service -Name $name -ErrorAction SilentlyContinue - if ($service) { - switch ($service.Status) { - 'Running' { $status = 'Running' } - 'Stopped' { $status = 'Stopped' } - 'Paused' { $status = 'Paused' } - 'StartPending' { $status = 'Starting' } - 'StopPending' { $status = 'Stopping' } - 'PausePending' { $status = 'Pausing' } - 'ContinuePending' { $status = 'Resuming' } - default { $status = 'Unknown' } + elseif ($IsWindows) { + # Check if the service exists on Windows + $service = Get-Service -Name $name -ErrorAction SilentlyContinue + if ($service) { + switch ($service.Status) { + 'Running' { $status = 'Running' } + 'Stopped' { $status = 'Stopped' } + 'Paused' { $status = 'Paused' } + 'StartPending' { $status = 'Starting' } + 'StopPending' { $status = 'Stopping' } + 'PausePending' { $status = 'Pausing' } + 'ContinuePending' { $status = 'Resuming' } + default { $status = 'Unknown' } + } + return @{ + Name = $name + Status = $status + } + } + else { + Write-PodeErrorLog -Message "Service '$name' not found on Windows." + return $null + } + } + + elseif ($IsLinux) { + try { + # Check if the service exists on Linux (systemd) + $output = systemctl is-active "$name.service" 2>&1 + if ($LASTEXITCODE -eq 0) { + if ($output -match 'active') { + $status = 'Running' + } + elseif ($output -match 'inactive \(dead\)') { + $status = 'Stopped' + } + elseif ($output -match 'activating') { + $status = 'Starting' + } + elseif ($output -match 'deactivating') { + $status = 'Stopping' + } + else { + $status = 'Unknown' } return @{ Name = $name @@ -592,34 +610,28 @@ function Get-PodeService { } } else { - Write-PodeErrorLog -Message "Service '$name' not found on Windows." - return $null + return @{ + Name = $name + Status = 'Stopped' + } } } + catch { + $_ | Write-PodeErrorLog + return $null + } + } - Unix { - try { - # Check if the service exists on Linux (systemd) - $output = systemctl is-active "$name.service" 2>&1 - if ($LASTEXITCODE -eq 0) { - if ($output -match 'active') { - $status = 'Running' - } - elseif ($output -match 'inactive \(dead\)') { - $status = 'Stopped' - } - elseif ($output -match 'activating') { - $status = 'Starting' - } - elseif ($output -match 'deactivating') { - $status = 'Stopping' - } - else { - $status = 'Unknown' - } + elseif ($IsMacOS) { + try { + # Check if the service exists on macOS (launchctl) + $serviceList = launchctl list | Select-String "pode.$name" + if ($serviceList) { + $status = launchctl list "pode.$name" 2>&1 + if ($status -match 'PID = (\d+)') { return @{ Name = $name - Status = $status + Status = 'Running' } } else { @@ -629,40 +641,14 @@ function Get-PodeService { } } } - catch { - $_ | Write-PodeErrorLog + else { + Write-PodeErrorLog -Message "Service 'pode.$name' not found on macOS." return $null } } - - MacOSX { - try { - # Check if the service exists on macOS (launchctl) - $serviceList = launchctl list | Select-String "pode.$name" - if ($serviceList) { - $status = launchctl list "pode.$name" 2>&1 - if ($status -match 'PID = (\d+)') { - return @{ - Name = $name - Status = 'Running' - } - } - else { - return @{ - Name = $name - Status = 'Stopped' - } - } - } - else { - Write-PodeErrorLog -Message "Service 'pode.$name' not found on macOS." - return $null - } - } - catch { - $_ | Write-PodeErrorLog - return $null - } + catch { + $_ | Write-PodeErrorLog + return $null } } } From 430ea27bcae47d5fc93fb74c5781b28698ea0b32 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 17 Oct 2024 16:16:02 -0700 Subject: [PATCH 06/93] FIx Mac service --- .gitignore | 1 + examples/HelloService/HelloService.ps1 | 8 +- examples/HelloService/srvsettings.json | 12 - src/Private/Service.ps1 | 63 ++-- src/Public/Service.ps1 | 414 +++++++++++++++---------- 5 files changed, 280 insertions(+), 218 deletions(-) delete mode 100644 examples/HelloService/srvsettings.json diff --git a/.gitignore b/.gitignore index 079cb2f3a..81f5c3014 100644 --- a/.gitignore +++ b/.gitignore @@ -266,3 +266,4 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 docs/Getting-Started/Samples.md +examples/HelloService/HelloService_srvsettings.json diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 8b43e3abc..5817773c3 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -59,21 +59,21 @@ if ($Register.IsPresent) { exit } if ($Unregister.IsPresent) { - Unregister-PodeService + Unregister-PodeService -Name 'HelloService' exit } if ($Start.IsPresent) { - Start-PodeService + Start-PodeService -Name 'HelloService' exit } if ($Stop.IsPresent) { - Stop-PodeService + Stop-PodeService -Name 'HelloService' exit } if ($Query.IsPresent) { - Get-PodeService + Get-PodeService -Name 'HelloService' exit } # Alternatively, you can directly import the Pode module from the system diff --git a/examples/HelloService/srvsettings.json b/examples/HelloService/srvsettings.json deleted file mode 100644 index 72aec77b4..000000000 --- a/examples/HelloService/srvsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "PodePwshWorker": { - "ScriptPath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", - "DisableTermination": true, - "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService_svc.log", - "Quiet": true, - "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", - "Name": "HelloService", - "ShutdownWaitTimeMs": 30000, - "ParameterString": "" - } -} diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 593b8eaf6..a649de2f4 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -220,15 +220,26 @@ function Get-PodeServiceName { function Register-PodeMacService { param( [Parameter(Mandatory = $true)] - [string]$Name, - - [string]$Description, - [string]$BinPath, - [string]$SettingsFile, - [string]$User, - [switch]$Start, - [switch]$Autostart, - [string]$OsArchitecture + [string] + $Name, + + [string] + $Description, + + [string] + $BinPath, + + [string] + $SettingsFile, + + [string] + $User, + + [string] + $OsArchitecture, + + [string] + $LogPath ) # Check if the service is already registered @@ -261,16 +272,20 @@ function Register-PodeMacService { $runAtLoad StandardOutPath - /var/log/pode.$Name.stdout.log + $LogPath/pode.$Name.stdout.log StandardErrorPath - /var/log/pode.$Name.stderr.log + $LogPath/pode.$Name.stderr.log KeepAlive - + + SuccessfulExit + + "@ | Set-Content -Path "~/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 + try { # Load the plist with launchctl launchctl load ~/Library/LaunchAgents/pode.$($Name).plist @@ -285,11 +300,6 @@ function Register-PodeMacService { throw $_ # Rethrow the error after logging } - # Optionally start the service - if ($Start.IsPresent) { - Start-PodeService - } - return $true } @@ -321,9 +331,6 @@ function Register-PodeMacService { .PARAMETER Group The group under which the service will run. Defaults to the same as the `User` parameter. -.PARAMETER Start - A switch indicating whether to start the service immediately after it is registered. - .PARAMETER SkipUserCreation A switch to skip the creation of the user if it does not exist. @@ -431,11 +438,6 @@ WantedBy=multi-user.target throw $_ # Rethrow the error after logging } - # Optionally start the service - if ($Start.IsPresent) { - Start-PodeService - } - return $true } @@ -474,9 +476,6 @@ WantedBy=multi-user.target .PARAMETER SecurityDescriptorSddl An SDDL string (Security Descriptor Definition Language) used to define the security of the service. -.PARAMETER Start - A switch to start the service immediately after it is registered. - .PARAMETER OsArchitecture The architecture of the operating system (e.g., `x64`, `arm64`). Used to locate the appropriate binary. @@ -529,9 +528,6 @@ function Register-PodeWindowsService { [string] $SecurityDescriptorSddl, - [switch] - $Start, - [string] $OsArchitecture ) @@ -568,10 +564,7 @@ function Register-PodeWindowsService { $_ | Write-PodeErrorLog throw $_ # Rethrow the error after logging } - if ($Start.IsPresent) { - # Start the service - Start-PodeService - } + return $true } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 8a5d44cce..e12d44d6e 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -106,98 +106,120 @@ function Register-PodeService { $SkipUserCreation, [pscredential] - $Credential + $Credential, + + [string] + $ConfigDirectory ) + try { + if ($MyInvocation.ScriptName) { + $ScriptPath = $MyInvocation.ScriptName + $MainScriptPath = Split-Path -Path $ScriptPath -Parent + # $MainScriptFileName = Split-Path -Path $ScriptPath -Leaf + } + else { + return $null + } - if ($MyInvocation.ScriptName) { - $ScriptPath = $MyInvocation.ScriptName - $MainScriptPath = Split-Path -Path $ScriptPath -Parent - # $MainScriptFileName = Split-Path -Path $ScriptPath -Leaf - } - else { - return $null - } + # Define script and log file paths + # $ScriptPath = Join-Path -Path $MainScriptPath -ChildPath $MainScriptFileName # Example script path + $LogPath = Join-Path -Path $MainScriptPath -ChildPath 'logs' + $LogFilePath = Join-Path -Path $LogPath -ChildPath "$($Name)_svc.log" - # Define script and log file paths - # $ScriptPath = Join-Path -Path $MainScriptPath -ChildPath $MainScriptFileName # Example script path - $LogPath = Join-Path -Path $MainScriptPath -ChildPath '/logs' - $LogFilePath = Join-Path -Path $LogPath -ChildPath "$($Name)_svc.log" + # Ensure log directory exists + if (-not (Test-Path $LogPath)) { + $null = New-Item -Path $LogPath -ItemType Directory -Force + } - # Ensure log directory exists - if (-not (Test-Path $LogPath)) { - New-Item -Path $LogPath -ItemType Directory -Force - } + # Obtain the PowerShell path dynamically + $PwshPath = (Get-Process -Id $PID).Path - # Obtain the PowerShell path dynamically - $PwshPath = (Get-Process -Id $PID).Path - - # Define the settings file path - $settingsFile = Join-Path -Path $MainScriptPath -ChildPath 'srvsettings.json' - - $binPath = Join-Path -path (Split-Path -Parent -Path $PSScriptRoot) -ChildPath 'Bin' - - # JSON content for the service settings - $jsonContent = @{ - PodePwshWorker = @{ - ScriptPath = $ScriptPath - PwshPath = $PwshPath - ParameterString = $ParameterString - LogFilePath = $LogFilePath - Quiet = $Quiet - DisableTermination = $DisableTermination - ShutdownWaitTimeMs = $ShutdownWaitTimeMs - Name = $Name + # Define the settings file path + if ($ConfigDirectory) { + $settingsPath = Join-Path -Path $MainScriptPath -ChildPath $ConfigDirectory + if (! (Test-Path -Path $settingsPath -PathType Container)) { + $null = New-Item -Path $settingsPath -ItemType Directory + } + } + else { + $settingsPath = $MainScriptPath + } + $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_srvsettings.json" + + $binPath = Join-Path -path (Split-Path -Parent -Path $PSScriptRoot) -ChildPath 'Bin' + + # JSON content for the service settings + $jsonContent = @{ + PodePwshWorker = @{ + ScriptPath = $ScriptPath + PwshPath = $PwshPath + ParameterString = $ParameterString + LogFilePath = $LogFilePath + Quiet = $Quiet + DisableTermination = $DisableTermination + ShutdownWaitTimeMs = $ShutdownWaitTimeMs + Name = $Name + } } - } - # Convert hash table to JSON and save it to the settings file - $jsonContent | ConvertTo-Json | Set-Content -Path $settingsFile -Encoding UTF8 + # Convert hash table to JSON and save it to the settings file + $jsonContent | ConvertTo-Json | Set-Content -Path $settingsFile -Encoding UTF8 - $osArchitecture = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() + $osArchitecture = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() - # Call the appropriate platform-specific function - if ($IsWindows) { - $param = @{ - Name = $Name - Description = $Description - DisplayName = $DisplayName - StartupType = $StartupType - BinPath = $binPath - SettingsFile = $settingsFile - Credential = $Credential - SecurityDescriptorSddl = $SecurityDescriptorSddl - Start = $Start - OsArchitecture = $osArchitecture + # Call the appropriate platform-specific function + if ($IsWindows) { + $param = @{ + Name = $Name + Description = $Description + DisplayName = $DisplayName + StartupType = $StartupType + BinPath = $binPath + SettingsFile = $settingsFile + Credential = $Credential + SecurityDescriptorSddl = $SecurityDescriptorSddl + OsArchitecture = "win-$osArchitecture" + } + $operation = Register-PodeWindowsService @param } - Register-PodeWindowsService @param - } - elseif ($IsLinux) { - $param = @{ - Name = $Name - Description = $Description - BinPath = $binPath - SettingsFile = $settingsFile - User = $User - Group = $Group - Start = $Start - SkipUserCreation = $SkipUserCreation - OsArchitecture = $osArchitecture + elseif ($IsLinux) { + $param = @{ + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + Group = $Group + Start = $Start + SkipUserCreation = $SkipUserCreation + OsArchitecture = "linux-$osArchitecture" + } + $operation = Register-PodeLinuxService @param } - Register-PodeLinuxService @param - } - elseif ($IsMacOS) { - $param = @{ - Name = $Name - Description = $Description - BinPath = $binPath - SettingsFile = $settingsFile - User = $User - Start = $Start - OsArchitecture = $osArchitecture + elseif ($IsMacOS) { + $param = @{ + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + OsArchitecture = "osx-$osArchitecture" + LogPath = $LogPath + } + + $operation = Register-PodeMacService @param } + # Optionally start the service + if ( $operation -and $Start.IsPresent) { + $operation = Start-PodeService -Name $Name + } - Register-PodeMacService @param + return $operation + } + catch { + $_ | Write-PodeErrorLog + return $false } } @@ -211,8 +233,8 @@ function Register-PodeService { It works on Windows, Linux (systemd), and macOS (launchctl), handling platform-specific service commands to start the service. If the service is not registered, it will throw an error. -.PARAMETER None - No parameters are required for this function. +.PARAMETER Name + The name of the service. .EXAMPLE Start-PodeService @@ -228,66 +250,77 @@ function Register-PodeService { - If the service is not registered, the function throws an error. #> function Start-PodeService { + param( + [string] + $Name + ) try { - # Get the service name from the settings file - $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + if (! $Name) { + # Get the service name from the settings file + $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + } if ($IsWindows) { # Get the Windows service - $service = Get-Service -Name $name -ErrorAction SilentlyContinue + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is already running if ($service.Status -ne 'Running') { - Start-Service -Name $name -ErrorAction Stop + Start-Service -Name $Name -ErrorAction Stop # Log service started successfully - # Write-PodeServiceLog -Message "Service '$name' started successfully." + # Write-PodeServiceLog -Message "Service '$Name' started successfully." } else { # Log service is already running - # Write-PodeServiceLog -Message "Service '$name' is already running." + # Write-PodeServiceLog -Message "Service '$Name' is already running." } } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } } elseif ($IsLinux) { # Check if the service exists - if (systemctl status "$name.service" -q) { + if (systemctl status "$Name.service" -q) { # Check if the service is already running - $status = systemctl is-active "$name.service" + $status = systemctl is-active "$Name.service" if ($status -ne 'active') { - systemctl start "$name.service" + systemctl start "$Name.service" # Log service started successfully - # Write-PodeServiceLog -Message "Service '$name' started successfully." + # Write-PodeServiceLog -Message "Service '$Name' started successfully." } else { # Log service is already running - # Write-PodeServiceLog -Message "Service '$name' is already running." + # Write-PodeServiceLog -Message "Service '$Name' is already running." } } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } } elseif ($IsMacOS) { # Check if the service exists in launchctl - if (launchctl list | Select-String "pode.$name") { - # Check if the service is already running - if (-not (launchctl list "pode.$name" | Select-String "pode.$name")) { - launchctl start "pode.$name" + if (launchctl list | Select-String "pode.$Name") { + + $serviceInfo = launchctl list "pode.$Name" -join "`n" + + # Check if the service has a PID entry + if (!($serviceInfo -match '"PID" = (\d+);')) { + launchctl start "pode.$Name" + # Log service started successfully - # Write-PodeServiceLog -Message "Service '$name' started successfully." + # Write-PodeServiceLog -Message "Service '$Name' started successfully." + return ($LASTEXITCODE -eq 0) } else { # Log service is already running - # Write-PodeServiceLog -Message "Service '$name' is already running." + # Write-PodeServiceLog -Message "Service '$Name' is already running." } } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } } } @@ -307,8 +340,8 @@ function Start-PodeService { If the service is running, it will attempt to stop the service gracefully. The function works on Windows, Linux (systemd), and macOS (launchctl). -.PARAMETER None - No parameters are required for this function. +.PARAMETER Name + The name of the service. .EXAMPLE Stop-PodeService @@ -323,57 +356,67 @@ function Start-PodeService { - If the service is not registered, the function throws an error. #> function Stop-PodeService { + param( + [string] + $Name + ) try { - # Get the service name from the settings file - $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + if (! $Name) { + # Get the service name from the settings file + $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + } if ($IsWindows) { - $service = Get-Service -Name $name -ErrorAction SilentlyContinue + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running if ($service.Status -eq 'Running') { - Stop-Service -Name $name -ErrorAction Stop -WarningAction SilentlyContinue - # Write-PodeServiceLog -Message "Service '$name' stopped successfully." + Stop-Service -Name $Name -ErrorAction Stop -WarningAction SilentlyContinue + # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." } else { - # Write-PodeServiceLog -Message "Service '$name' is not running." + # Write-PodeServiceLog -Message "Service '$Name' is not running." } } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } } elseif ($IsLinux) { # Check if the service exists - if (systemctl status "$name.service" -q) { - $status = systemctl is-active "$name.service" + if (systemctl status "$Name.service" -q) { + $status = systemctl is-active "$Name.service" if ($status -eq 'active') { - systemctl stop "$name.service" - # Write-PodeServiceLog -Message "Service '$name' stopped successfully." + systemctl stop "$Name.service" + # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." } else { - # Write-PodeServiceLog -Message "Service '$name' is not running." + # Write-PodeServiceLog -Message "Service '$Name' is not running." } } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } } elseif ($IsMacOS) { # Check if the service exists in launchctl - if (launchctl list | Select-String "pode.$name") { + if (launchctl list | Select-String "pode.$Name") { # Stop the service if running - if (launchctl list "pode.$name" | Select-String "pode.$name") { - launchctl stop "pode.$name" - # Write-PodeServiceLog -Message "Service '$name' stopped successfully." + $serviceInfo = launchctl list "pode.$Name" -join "`n" + + # Check if the service has a PID entry + if ($serviceInfo -match '"PID" = (\d+);') { + launchctl stop "pode.$Name" + # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + return ($LASTEXITCODE -eq 0) } else { - # Write-PodeServiceLog -Message "Service '$name' is not running." + # Write-PodeServiceLog -Message "Service '$Name' is not running." } } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } } } @@ -397,6 +440,9 @@ function Stop-PodeService { 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. + .EXAMPLE Unregister-PodeService -Force @@ -416,34 +462,39 @@ function Stop-PodeService { function Unregister-PodeService { param( [Parameter()] - [switch]$Force - ) + [switch]$Force, - # Get the service name from the settings file - $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + [string] + $Name + ) + if (! $Name) { + # Get the service name from the settings file + $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + } if ($IsWindows) { # Check if the service exists - $service = Get-Service -Name $name -ErrorAction SilentlyContinue + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if (-not $service) { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } try { # Check if the service is running before attempting to stop it if ($service.Status -eq 'Running') { if ($Force.IsPresent) { - Stop-Service -Name $name -Force -ErrorAction Stop - # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + Stop-Service -Name $Name -Force -ErrorAction Stop + # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { - throw "Service '$name' is running. Use the -Force parameter to forcefully stop." + throw "Service '$Name' is running. Use the -Force parameter to forcefully stop." } } # Remove the service - Remove-Service -Name $name -ErrorAction Stop - # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." + Remove-Service -Name $Name -ErrorAction Stop + # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + return $true } catch { @@ -455,24 +506,24 @@ function Unregister-PodeService { elseif ($IsLinux) { try { # Check if the service exists - if (systemctl status "$name.service" -q) { + if (systemctl status "$Name.service" -q) { # Check if the service is running - $status = systemctl is-active "$name.service" + $status = systemctl is-active "$Name.service" if ($status -eq 'active') { if ($Force.IsPresent) { - systemctl stop "$name.service" - # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + systemctl stop "$Name.service" + # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { - throw "Service '$name' is running. Use the -Force parameter to forcefully stop." + throw "Service '$Name' is running. Use the -Force parameter to forcefully stop." } } - systemctl disable "$name.service" - Remove-Item "/etc/systemd/system/$name.service" - # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." + systemctl disable "$Name.service" + Remove-Item "/etc/systemd/system/$Name.service" + # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } return $true } @@ -485,23 +536,41 @@ function Unregister-PodeService { elseif ($IsMacOS) { try { # Check if the service exists - if (launchctl list | Select-String "pode.$name") { + + if (launchctl list | Select-String "pode.$Name") { + $serviceInfo = launchctl list "pode.$Name" -join "`n" + # Check if the service has a PID entry + if ($serviceInfo -match '"PID" = (\d+);') { + launchctl stop "pode.$Name" + # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + $serviceIsRunning = ($LASTEXITCODE -ne 0) + } + else { + $serviceIsRunning = $false + # Write-PodeServiceLog -Message "Service '$Name' is not running." + } + # Check if the service is running - if (launchctl list "pode.$name" | Select-String "pode.$name") { + if ( $serviceIsRunning) { if ($Force.IsPresent) { - launchctl stop "pode.$name" - # Write-PodeServiceLog -Message "Service '$name' stopped forcefully." + launchctl stop "pode.$Name" + # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { - throw "Service '$name' is running. Use the -Force parameter to forcefully stop." + throw "Service '$Name' is running. Use the -Force parameter to forcefully stop." } } - launchctl unload "/Library/LaunchDaemons/pode.$name.plist" - Remove-Item "~/Library/LaunchAgents/pode.$name.plist" - # Write-PodeServiceLog -Message "Service '$name' unregistered successfully." + launchctl unload ~/Library/LaunchAgents/pode.$Name.plist + if ($LASTEXITCODE -eq 0) { + Remove-Item "~/Library/LaunchAgents/pode.$Name.plist" -ErrorAction Break + } + else { + return $false + } + # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } else { - throw "Service '$name' is not registered." + throw "Service '$Name' is not registered." } return $true } @@ -523,9 +592,8 @@ function Unregister-PodeService { 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 None - This function does not accept any parameters directly, but it relies on the service name from the configuration file - (`srvsettings.json`) located in the script's directory. +.PARAMETER Name + The name of the service. .OUTPUTS Hashtable @@ -556,12 +624,19 @@ function Unregister-PodeService { - For macOS, it uses `launchctl` to check if the service is running. #> function Get-PodeService { + param( + [string] + $Name + ) - $name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + if (! $Name) { + # Get the service name from the settings file + $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) + } - elseif ($IsWindows) { + if ($IsWindows) { # Check if the service exists on Windows - $service = Get-Service -Name $name -ErrorAction SilentlyContinue + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { switch ($service.Status) { 'Running' { $status = 'Running' } @@ -574,12 +649,12 @@ function Get-PodeService { default { $status = 'Unknown' } } return @{ - Name = $name + Name = $Name Status = $status } } else { - Write-PodeErrorLog -Message "Service '$name' not found on Windows." + Write-PodeErrorLog -Message "Service '$Name' not found on Windows." return $null } } @@ -587,7 +662,7 @@ function Get-PodeService { elseif ($IsLinux) { try { # Check if the service exists on Linux (systemd) - $output = systemctl is-active "$name.service" 2>&1 + $output = systemctl is-active "$Name.service" 2>&1 if ($LASTEXITCODE -eq 0) { if ($output -match 'active') { $status = 'Running' @@ -605,13 +680,13 @@ function Get-PodeService { $status = 'Unknown' } return @{ - Name = $name + Name = $Name Status = $status } } else { return @{ - Name = $name + Name = $Name Status = 'Stopped' } } @@ -625,24 +700,29 @@ function Get-PodeService { elseif ($IsMacOS) { try { # Check if the service exists on macOS (launchctl) - $serviceList = launchctl list | Select-String "pode.$name" + $serviceList = launchctl list | Select-String "pode.$Name" if ($serviceList) { - $status = launchctl list "pode.$name" 2>&1 - if ($status -match 'PID = (\d+)') { + $serviceInfo = launchctl list "pode.$Name" -join "`n" + $running = $serviceInfo -match '"PID" = (\d+);' + # Check if the service has a PID entry + if ($running) { + $servicePid = ($running[0].split('= '))[1].trim(';') # Extract the PID from the match + return @{ - Name = $name + Name = $Name Status = 'Running' + Pid = $servicePid } } else { return @{ - Name = $name + Name = $Name Status = 'Stopped' } } } else { - Write-PodeErrorLog -Message "Service 'pode.$name' not found on macOS." + Write-PodeErrorLog -Message "Service 'pode.$Name' not found on macOS." return $null } } From b87561e559458fffca7ada7d7107159d3504f97f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 17 Oct 2024 17:28:47 -0700 Subject: [PATCH 07/93] Fix Windows --- examples/HelloService/HelloService.ps1 | 10 ++--- .../HelloService2_srvsettings.json | 12 +++++ pode.build.ps1 | 25 ----------- src/Private/Helpers.ps1 | 45 ++++++++++++++++++- src/Private/Service.ps1 | 14 +++++- src/Public/Service.ps1 | 37 +++++++++++++-- 6 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 examples/HelloService/HelloService2_srvsettings.json diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 5817773c3..c477d76f9 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -55,25 +55,25 @@ catch { if ($Register.IsPresent) { - Register-PodeService -Name 'HelloService' -Start + Register-PodeService -Name 'HelloService2' -Start exit } if ($Unregister.IsPresent) { - Unregister-PodeService -Name 'HelloService' + Unregister-PodeService -Name 'HelloService2' exit } if ($Start.IsPresent) { - Start-PodeService -Name 'HelloService' + Start-PodeService -Name 'HelloService2' exit } if ($Stop.IsPresent) { - Stop-PodeService -Name 'HelloService' + Stop-PodeService -Name 'HelloService2' exit } if ($Query.IsPresent) { - Get-PodeService -Name 'HelloService' + Get-PodeService -Name 'HelloService2' exit } # Alternatively, you can directly import the Pode module from the system diff --git a/examples/HelloService/HelloService2_srvsettings.json b/examples/HelloService/HelloService2_srvsettings.json new file mode 100644 index 000000000..ef95d025b --- /dev/null +++ b/examples/HelloService/HelloService2_srvsettings.json @@ -0,0 +1,12 @@ +{ + "PodePwshWorker": { + "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + "Quiet": true, + "DisableTermination": true, + "ParameterString": "", + "Name": "HelloService2", + "ScriptPath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", + "ShutdownWaitTimeMs": 30000, + "LogFilePath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService2_svc.log" + } +} diff --git a/pode.build.ps1 b/pode.build.ps1 index 18c8a2731..3b78b79e1 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -171,31 +171,6 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { } } - # Check if 'lipo' exists - $lipoExists = Get-Command lipo -ErrorAction SilentlyContinue - - if ($lipoExists) { - # Define the paths for the x64 and arm64 binaries and the universal output - $osxX64Path = '../Bin/osx-x64/PodeMonitor' - $osxArm64Path = '../Bin/osx-arm64/PodeMonitor' - $universalPath = '../Bin/osx-universal/PodeMonitor' - # Run 'lipo' to combine x64 and arm64 binaries into a universal binary - $lipoCommand = "lipo -create $osxX64Path $osxArm64Path -output $universalPath" - Write-Host 'Running lipo to create universal binary...' - - # Run the lipo command - try { - Invoke-Expression $lipoCommand - Write-Host "Universal binary created at: $universalPath" - } - catch { - Write-Host "Failed to create universal binary: $_" - } - } - else { - Write-Host "'lipo' not found. Please install 'lipo' to create a universal binary." - } - } function Get-PodeBuildPwshEOL { diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 0bdc9237b..5bdf017bd 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3775,4 +3775,47 @@ function Copy-PodeObjectDeepClone { # Deserialize the XML back into a new PSObject, creating a deep clone of the original return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) } -} \ No newline at end of file +} + + +<# +.SYNOPSIS + Checks if the current user has administrative privileges on Windows. + +.DESCRIPTION + The `Test-PodeIsAdmin` function verifies if the current user has the necessary + privileges to perform administrative tasks by checking if they belong to the + Windows Administrator role. It will only run on Windows and returns `$true` if + the user has administrative privileges, otherwise `$false`. + + If executed on a non-Windows platform, it returns `$false` and displays a message + indicating that the function is only applicable to Windows. + +.EXAMPLE + PS> Test-PodeIsAdmin + True + + This command checks if the current user is an administrator on a Windows system. + +.EXAMPLE + PS> if (Test-PodeIsAdmin) { "User has admin rights" } else { "User does not have admin rights" } + + This command conditionally outputs whether the current user has administrative rights + on Windows. If the script is run on a non-Windows system, it outputs "User does not + have admin rights." + +.NOTES + This function will only check for administrative privileges if executed on a Windows system. +#> +function Test-PodeIsAdmin { + # Check if the operating system is Windows + if ($IsWindows -ne $true) { + return $false + } + + $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($currentUser) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + + \ No newline at end of file diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index a649de2f4..a7979e38b 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -541,7 +541,7 @@ function Register-PodeWindowsService { # Parameters for New-Service $params = @{ Name = $Name - BinaryPathName = "`"$BinPath\win-$OsArchitecture\PodeMonitor.exe`" `"$SettingsFile`"" + BinaryPathName = "`"$BinPath\$OsArchitecture\PodeMonitor.exe`" `"$SettingsFile`"" DisplayName = $DisplayName StartupType = $StartupType Description = $Description @@ -665,4 +665,16 @@ function Write-PodeServiceLog { $logItem | ConvertTo-Json -Compress -Depth 5 | Add-Content "$lpath/watchdog-$($Service.Name).log" } +} + + +function Test-PodeUserServiceCreationPrivilege { + # Get the list of user privileges + $privileges = whoami /priv | Where-Object { $_ -match "SeCreateServicePrivilege" } + + if ($privileges) { + return $true + } else { + return $false + } } \ No newline at end of file diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index e12d44d6e..d39a519f0 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -112,6 +112,15 @@ function Register-PodeService { $ConfigDirectory ) try { + + if ($IsWindows) { + # Check if the current script is running as Administrator + if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { + Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow + exit + } + } + if ($MyInvocation.ScriptName) { $ScriptPath = $MyInvocation.ScriptName $MainScriptPath = Split-Path -Path $ScriptPath -Parent @@ -261,6 +270,13 @@ function Start-PodeService { } if ($IsWindows) { + + # Check if the current script is running as Administrator + if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { + Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow + exit + } + # Get the Windows service $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { @@ -367,6 +383,10 @@ function Stop-PodeService { } if ($IsWindows) { + if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { + Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow + exit + } $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running @@ -473,6 +493,10 @@ function Unregister-PodeService { $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) } if ($IsWindows) { + if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { + Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow + exit + } # Check if the service exists $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if (-not $service) { @@ -635,10 +659,15 @@ function Get-PodeService { } if ($IsWindows) { + if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { + Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow + exit + } # Check if the service exists on Windows - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" + if ($service) { - switch ($service.Status) { + switch ($service.State) { 'Running' { $status = 'Running' } 'Stopped' { $status = 'Stopped' } 'Paused' { $status = 'Paused' } @@ -651,10 +680,11 @@ function Get-PodeService { return @{ Name = $Name Status = $status + Pid = $service.ProcessId } } else { - Write-PodeErrorLog -Message "Service '$Name' not found on Windows." + #Write-PodeErrorLog -Message "Service '$Name' not found on Windows." return $null } } @@ -718,6 +748,7 @@ function Get-PodeService { return @{ Name = $Name Status = 'Stopped' + Pid = 0 } } } From 6c51a480e5433438594f1962c4b5c40385bddf4c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 18 Oct 2024 08:48:28 -0700 Subject: [PATCH 08/93] Fix tests --- src/Locales/ar/Pode.psd1 | 5 ++ src/Locales/de/Pode.psd1 | 5 ++ src/Locales/en-us/Pode.psd1 | 5 ++ src/Locales/en/Pode.psd1 | 5 ++ src/Locales/es/Pode.psd1 | 5 ++ src/Locales/fr/Pode.psd1 | 5 ++ src/Locales/it/Pode.psd1 | 5 ++ src/Locales/ja/Pode.psd1 | 5 ++ src/Locales/ko/Pode.psd1 | 5 ++ src/Locales/nl/Pode.psd1 | 5 ++ src/Locales/pl/Pode.psd1 | 5 ++ src/Locales/pt/Pode.psd1 | 5 ++ src/Locales/zh/Pode.psd1 | 5 ++ src/Private/Service.ps1 | 167 +++++++++++++----------------------- src/Public/Service.ps1 | 132 ++++++++++++++-------------- 15 files changed, 191 insertions(+), 173 deletions(-) diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 978646dab..76bff313a 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = 'لا يمكن أن تحتوي عمليات {0} على محتوى الطلب.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' + serviceAlreadyRegisteredException = "الخدمة '{0}' مسجلة بالفعل." + serviceNotRegisteredException = "الخدمة '{0}' غير مسجلة." + serviceRegistrationException = "الخدمة '{0}' غير مسجلة." + serviceRegistrationFailedException = "فشل تسجيل الخدمة '{0}'." + serviceIsRunningException = "الخدمة '{0}' تعمل. استخدم المعامل -Force للإيقاف بالقوة." } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index 4d70c92aa..1eb6d95ea 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = '{0}-Operationen können keinen Anforderungstext haben.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe." unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}' + serviceAlreadyRegisteredException = "Der Dienst '{0}' ist bereits registriert." + serviceNotRegisteredException = "Der Dienst '{0}' ist nicht registriert." + serviceRegistrationException = "Der Dienst '{0}' ist nicht registriert." + serviceRegistrationFailedException = "Die Registrierung des Dienstes '{0}' ist fehlgeschlagen." + serviceIsRunningException = "Der Dienst '{0}' läuft. Verwenden Sie den Parameter -Force, um den Dienst zwangsweise zu stoppen." } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 85ef1c3a5..e933f9124 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' + serviceAlreadyRegisteredException = "Service '{0}' is already registered." + serviceNotRegisteredException = "Service '{0}' is not registered." + serviceRegistrationException = "Service '{0}' is not registered." + serviceRegistrationFailedException = "Service '{0}' registration failed." + serviceIsRunningException = "Service '{0}' is running. Use the -Force parameter to forcefully stop." } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 4aa2f34f7..d795170c8 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' + serviceAlreadyRegisteredException = "Service '{0}' is already registered." + serviceNotRegisteredException = "Service '{0}' is not registered." + serviceRegistrationException = "Service '{0}' is not registered." + serviceRegistrationFailedException = "Service '{0}' registration failed." + serviceIsRunningException = "Service '{0}' is running. Use the -Force parameter to forcefully stop." } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 409357fe4..d46948e46 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = 'Las operaciones {0} no pueden tener un cuerpo de solicitud.' 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}' + serviceAlreadyRegisteredException = "El servicio '{0}' ya está registrado." + serviceNotRegisteredException = "El servicio '{0}' no está registrado." + serviceRegistrationException = "El servicio '{0}' no está registrado." + serviceRegistrationFailedException = "Falló el registro del servicio '{0}'." + serviceIsRunningException = "El servicio '{0}' está en ejecución. Utilice el parámetro -Force para detenerlo a la fuerza." } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index c543ae136..fa8b02f7b 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -291,5 +291,10 @@ getRequestBodyNotAllowedExceptionMessage = 'Les opérations {0} ne peuvent pas avoir de corps de requête.' 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." + serviceAlreadyRegisteredException = "Le service '{0}' est déjà enregistré." + serviceNotRegisteredException = "Le service '{0}' n'est pas enregistré." + serviceRegistrationException = "Le service '{0}' n'est pas enregistré." + serviceRegistrationFailedException = "É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." } diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index cbc7ebb2c..8b9f966bc 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = 'Le operazioni {0} non possono avere un corpo della richiesta.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non è supportata per la codifica {0}' + serviceAlreadyRegisteredException = "Il servizio '{0}' è già registrato." + serviceNotRegisteredException = "Il servizio '{0}' non è registrato." + serviceRegistrationException = "Il servizio '{0}' non è registrato." + serviceRegistrationFailedException = "Registrazione del servizio '{0}' non riuscita." + serviceIsRunningException = "Il servizio '{0}' è in esecuzione. Utilizzare il parametro -Force per interromperlo forzatamente." } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 5af361d24..22c78f4c8 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = '{0}操作にはリクエストボディを含めることはできません。' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。" unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' + serviceAlreadyRegisteredException = "サービス '{0}' はすでに登録されています。" + serviceNotRegisteredException = "サービス '{0}' は登録されていません。" + serviceRegistrationException = "サービス '{0}' は登録されていません。" + serviceRegistrationFailedException = "サービス '{0}' の登録に失敗しました。" + serviceIsRunningException = "サービス '{0}' が実行中です。強制的に停止するには、-Force パラメーターを使用してください。" } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 26dc8a116..5d0c475d1 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = '{0} 작업에는 요청 본문이 있을 수 없습니다.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' + serviceAlreadyRegisteredException = "서비스 '{0}'가 이미 등록되었습니다." + serviceNotRegisteredException = "서비스 '{0}'가 등록되지 않았습니다." + serviceRegistrationException = "서비스 '{0}'가 등록되지 않았습니다." + serviceRegistrationFailedException = "서비스 '{0}' 등록에 실패했습니다." + serviceIsRunningException = "서비스 '{0}'가 실행 중입니다. 강제로 중지하려면 -Force 매개변수를 사용하세요." } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 8e88fe7d5..f96b6ea0a 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = '{0}-operaties kunnen geen Request Body hebben.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer." unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}' + serviceAlreadyRegisteredException = "De service '{0}' is al geregistreerd." + serviceNotRegisteredException = "De service '{0}' is niet geregistreerd." + serviceRegistrationException = "De service '{0}' is niet geregistreerd." + serviceRegistrationFailedException = "Registratie van de service '{0}' is mislukt." + serviceIsRunningException = "De service '{0}' draait. Gebruik de parameter -Force om de service geforceerd te stoppen." } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index afbcb3dc2..ca9c11cde 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = 'Operacje {0} nie mogą mieć treści żądania.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku." unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}' + serviceAlreadyRegisteredException = "Usługa '{0}' jest już zarejestrowana." + serviceNotRegisteredException = "Usługa '{0}' nie jest zarejestrowana." + serviceRegistrationException = "Usługa '{0}' nie jest zarejestrowana." + serviceRegistrationFailedException = "Rejestracja usługi '{0}' nie powiodła się." + serviceIsRunningException = "Usługa '{0}' jest uruchomiona. Użyj parametru -Force, aby wymusić zatrzymanie." } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index af6d8731b..7dfc10bd2 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = 'As operações {0} não podem ter um corpo de solicitação.' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "A função '{0}' não aceita uma matriz como entrada de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'A codificação de compressão de fluxo não é suportada.' + serviceAlreadyRegisteredException = "O serviço '{0}' já está registrado." + serviceNotRegisteredException = "O serviço '{0}' não está registrado." + serviceRegistrationException = "O serviço '{0}' não está registrado." + serviceRegistrationFailedException = "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." } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 9e7652920..0ac6373c3 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -291,4 +291,9 @@ getRequestBodyNotAllowedExceptionMessage = '{0} 操作不能包含请求体。' fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' + serviceAlreadyRegisteredException = "服务 '{0}' 已经注册。" + serviceNotRegisteredException = "服务 '{0}' 未注册。" + serviceRegistrationException = "服务 '{0}' 未注册。" + serviceRegistrationFailedException = "服务 '{0}' 注册失败。" + serviceIsRunningException = "服务 '{0}' 正在运行。使用 -Force 参数强制停止。" } \ No newline at end of file diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index a7979e38b..ae2eb7da0 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -60,53 +60,53 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { Write-PodeServiceLog -Message "Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" - try { - # Create a named pipe server stream - $pipeStream = [System.IO.Pipes.NamedPipeServerStream]::new( - $PodeContext.Server.Service.PipeName, - [System.IO.Pipes.PipeDirection]::InOut, - 2, # Max number of allowed concurrent connections - [System.IO.Pipes.PipeTransmissionMode]::Byte, - [System.IO.Pipes.PipeOptions]::None - ) - - Write-PodeServiceLog -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." - $pipeStream.WaitForConnection() # Wait until a client connects - Write-PodeServiceLog -Message "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 - while ($pipeStream.IsConnected) { - $message = $reader.ReadLine() # Read message from the pipe - - if ($message) { - Write-PodeServiceLog -Message "Received message: $message" - - # Process 'shutdown' message - if ($message -eq 'shutdown') { - Write-PodeServiceLog -Message 'Server requested shutdown. Closing client...' - Close-PodeServer # Gracefully stop the Pode server - break # Exit the loop - - # Process 'restart' message - } - elseif ($message -eq 'restart') { - Write-PodeServiceLog -Message 'Server requested restart. Restarting client...' - Restart-PodeServer # Restart the Pode server - break # Exit the loop - } + try { + # Create a named pipe server stream + $pipeStream = [System.IO.Pipes.NamedPipeServerStream]::new( + $PodeContext.Server.Service.PipeName, + [System.IO.Pipes.PipeDirection]::InOut, + 2, # Max number of allowed concurrent connections + [System.IO.Pipes.PipeTransmissionMode]::Byte, + [System.IO.Pipes.PipeOptions]::None + ) + + Write-PodeServiceLog -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." + $pipeStream.WaitForConnection() # Wait until a client connects + Write-PodeServiceLog -Message "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 + while ($pipeStream.IsConnected) { + $message = $reader.ReadLine() # Read message from the pipe + + if ($message) { + Write-PodeServiceLog -Message "Received message: $message" + + # Process 'shutdown' message + if ($message -eq 'shutdown') { + Write-PodeServiceLog -Message 'Server requested shutdown. Closing client...' + Close-PodeServer # Gracefully stop the Pode server + break # Exit the loop + + # Process 'restart' message + } + elseif ($message -eq 'restart') { + Write-PodeServiceLog -Message 'Server requested restart. Restarting client...' + Restart-PodeServer # Restart the Pode server + break # Exit the loop } } } - catch { - $_ | Write-PodeServiceLog # Log any errors that occur during pipe operation - } - finally { - $reader.Dispose() - $pipeStream.Dispose() # Always dispose of the pipe stream when done - } + } + catch { + $_ | Write-PodeServiceLog # Log any errors that occur during pipe operation + } + finally { + $reader.Dispose() + $pipeStream.Dispose() # Always dispose of the pipe stream when done + } } @@ -119,60 +119,6 @@ function Start-PodeServiceHearthbeat { } } - - - -<# -.SYNOPSIS - Retrieves the service name from the `srvsettings.json` file located at the specified path. - -.DESCRIPTION - The `Get-PodeServiceName` function loads the service configuration from the provided path and retrieves the service name from the `srvsettings.json` file. - If the file does not exist or the name cannot be found in the file, an error is thrown. - -.PARAMETER Path - The directory path where the `srvsettings.json` file is located. - -.EXAMPLE - $serviceName = Get-PodeServiceName -Path "C:\PodeService" - - Retrieves the service name from the `srvsettings.json` file located at "C:\PodeService". - -.NOTES - This is an internal function and may change in future releases of Pode. - The function will throw an error if the settings file does not exist, is malformed, or the service name cannot be determined. -#> -function Get-PodeServiceName { - param( - [Parameter(Mandatory = $true)] - [string] - $Path - ) - - # Define the settings file path - $settingsFile = "$Path/srvsettings.json" - - - if (!(Test-Path -Path $settingsFile -PathType Leaf)) { - throw ($PodeLocale.pathNotExistExceptionMessage -f $settingsFile) - } - # Load the settings from the JSON file - try { - $settings = Get-Content -Path $settingsFile -Raw | ConvertFrom-Json - } - catch { - throw "Failed to load or parse the settings file '$settingsFile'. Error: $_" - } - - # Attempt to retrieve the name from the settings - if ($settings.PodePwshWorker -and $settings.PodePwshWorker.Name) { - return $settings.PodePwshWorker.Name - } - - throw ('Service name could not be determined from {0}' -f $settingsFile) - -} - <# .SYNOPSIS Registers a Pode service as a macOS LaunchAgent/Daemon. @@ -244,7 +190,8 @@ function Register-PodeMacService { # Check if the service is already registered if (launchctl list | Select-String "pode.$Name") { - throw 'Service is already registered.' + # Service is already registered. + throw ($PodeLocale.serviceAlreadyRegisteredException -f "pode.$Name") } # Determine whether the service should run at load @@ -292,7 +239,9 @@ function Register-PodeMacService { # Verify the service is now registered if (-not (launchctl list | Select-String "pode.$Name")) { - throw 'Service failed to register.' + # Service registration failed. + throw ($PodeLocale.serviceRegistrationFailedException -f "pode.$Name") + } } catch { @@ -394,7 +343,8 @@ function Register-PodeLinuxService { # Check if the service is already registered if (systemctl status "$Name.service" -ErrorAction SilentlyContinue) { - throw 'Service is already registered.' + # Service is already registered. + throw ($PodeLocale.serviceAlreadyRegisteredException -f "$Name.service" ) } # Create the service file @@ -430,7 +380,8 @@ WantedBy=multi-user.target try { systemctl enable "$Name.service" if ($LASTEXITCODE -ne 0) { - throw 'Service failed to register.' + # Service registration failed. + throw ($PodeLocale.serviceRegistrationFailedException -f "$Name.service") } } catch { @@ -535,7 +486,9 @@ function Register-PodeWindowsService { # Check if service already exists if (Get-Service -Name $Name -ErrorAction SilentlyContinue) { - throw 'Service is already registered.' + # Service is already registered. + throw ($PodeLocale.serviceAlreadyRegisteredException -f "$Name") + } # Parameters for New-Service @@ -557,7 +510,8 @@ function Register-PodeWindowsService { try { $sv = New-Service @params -ErrorAction SilentlyContinue -WarningAction SilentlyContinue if (!$sv) { - throw 'Service failed to register.' + # Service registration failed. + throw ($PodeLocale.serviceRegistrationFailedException -f "$Name") } } catch { @@ -670,11 +624,12 @@ function Write-PodeServiceLog { function Test-PodeUserServiceCreationPrivilege { # Get the list of user privileges - $privileges = whoami /priv | Where-Object { $_ -match "SeCreateServicePrivilege" } + $privileges = whoami /priv | Where-Object { $_ -match 'SeCreateServicePrivilege' } if ($privileges) { return $true - } else { + } + else { return $false } } \ No newline at end of file diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index d39a519f0..34f8223f4 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -1,66 +1,70 @@ <# .SYNOPSIS - Registers a new service to run a Pode-based PowerShell worker as a service on multiple platforms. + Registers a new service to run a Pode-based PowerShell worker as a service on Windows, Linux, or macOS. .DESCRIPTION - The `Register-PodeService` function configures and registers a service for running a Pode-based PowerShell worker on Windows, Linux, or macOS. - It dynamically sets up the service with the specified parameters, including paths to the script, log files, PowerShell executable, - and service settings. It also generates a `srvsettings.json` file containing the service's configuration and registers the service - using platform-specific methods. + The `Register-PodeService` function configures and registers a Pode-based service that runs a PowerShell worker across + multiple platforms (Windows, Linux, macOS). It dynamically creates a service with the specified parameters, including + paths to the worker script, log files, and service-specific settings. The function also generates a `srvsettings.json` file, + containing the service configuration. The service can optionally be started immediately after registration, based on the platform. .PARAMETER Name - The name of the service to be registered. + Specifies the name of the service to be registered. .PARAMETER Description A brief description of the service. Defaults to "This is a Pode service." .PARAMETER DisplayName - The display name of the service, as it will appear in the Windows Services Manager (Windows only). Defaults to "Pode Service($Name)". + Specifies the display name for the service in the Windows Services Manager. Defaults to "Pode Service($Name)". .PARAMETER StartupType - The startup type of the service (e.g., Automatic, Manual). Defaults to 'Automatic'. + Specifies the startup type of the service (e.g., 'Automatic', 'Manual'). Defaults to 'Automatic'. .PARAMETER SecurityDescriptorSddl - The security descriptor in SDDL format for the service (Windows only). + A security descriptor in SDDL format, specifying the permissions for the service (Windows only). .PARAMETER ParameterString - Any additional parameters to pass to the script when it is run by the service. Defaults to an empty string. + Any additional parameters to pass to the worker script when run by the service. Defaults to an empty string. .PARAMETER Quiet - A boolean value indicating whether to run the service quietly, suppressing logs and output. Defaults to `$true`. + If set to `$true`, runs the service quietly, suppressing logs and output. Defaults to `$true`. .PARAMETER DisableTermination - A boolean value indicating whether to disable termination of the service from within the worker process. Defaults to `$true`. + If set to `$true`, disables termination of the service from within the worker process. Defaults to `$true`. .PARAMETER ShutdownWaitTimeMs - The maximum amount of time, in milliseconds, to wait for the service to gracefully shut down before forcefully terminating it. Defaults to 30,000 milliseconds. + The maximum amount of time, in milliseconds, to wait for the service to shut down gracefully before forcefully terminating it. + Defaults to 30,000 milliseconds (30 seconds). .PARAMETER User - The user under which the service should run. Defaults to `podeuser`. + Specifies the user under which the service will run (applies to Linux and macOS). Defaults to `podeuser`. .PARAMETER Group - The group under which the service should run (Linux only). Defaults to `podeuser`. + Specifies the group under which the service will run (Linux only). Defaults to `podeuser`. .PARAMETER Start - A switch to start the service immediately after it is registered. + A switch to immediately start the service after registration. .PARAMETER SkipUserCreation - A switch to skip the user creation process (Linux only). + A switch to skip the process of creating a new user (Linux only). .PARAMETER Credential - A `PSCredential` object specifying the credentials for the account under which the Windows service will run. + A `PSCredential` object specifying the credentials for the Windows service account under which the service will run. + +.PARAMETER ConfigDirectory + Specifies a custom directory to store the generated configuration (`srvsettings.json`) file. .EXAMPLE Register-PodeService -Name "PodeExampleService" -Description "Example Pode Service" -ParameterString "-Verbose" - Registers a new Pode-based service called "PodeExampleService" with verbose logging enabled. + This example registers a new Pode service called "PodeExampleService" with verbose logging enabled. .NOTES - - This function is cross-platform and handles service registration on Windows, Linux, and macOS. - - A `srvsettings.json` file is generated in the same directory as the main script, containing the configuration for the Pode service. - - The function checks if a service with the specified name already exists on the respective platform and throws an error if it does. - - For Windows, the service binary path points to the Pode monitor executable (`PodeMonitor.exe`), which is located in the `Bin` directory relative to the script. - - This function dynamically determines the PowerShell executable path and system architecture. + - The function supports cross-platform service registration on Windows, Linux, and macOS. + - A configuration file (`srvsettings.json`) is generated in the specified directory, or by default, in the same directory as the main script. + - On Windows, the function checks for appropriate permissions (e.g., Administrator or service creation privileges). + - The Pode service can be started automatically after registration using the `-Start` switch. + - The PowerShell executable path is dynamically obtained to ensure compatibility across environments. #> function Register-PodeService { param( @@ -112,38 +116,35 @@ function Register-PodeService { $ConfigDirectory ) try { - + # Check for administrative privileges on Windows if ($IsWindows) { - # Check if the current script is running as Administrator if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow exit } } + # Obtain the script path and directory if ($MyInvocation.ScriptName) { $ScriptPath = $MyInvocation.ScriptName $MainScriptPath = Split-Path -Path $ScriptPath -Parent - # $MainScriptFileName = Split-Path -Path $ScriptPath -Leaf } else { return $null } - # Define script and log file paths - # $ScriptPath = Join-Path -Path $MainScriptPath -ChildPath $MainScriptFileName # Example script path + # Define log paths and ensure the log directory exists $LogPath = Join-Path -Path $MainScriptPath -ChildPath 'logs' $LogFilePath = Join-Path -Path $LogPath -ChildPath "$($Name)_svc.log" - # Ensure log directory exists if (-not (Test-Path $LogPath)) { $null = New-Item -Path $LogPath -ItemType Directory -Force } - # Obtain the PowerShell path dynamically + # Dynamically get the PowerShell executable path $PwshPath = (Get-Process -Id $PID).Path - # Define the settings file path + # Define configuration directory and settings file path if ($ConfigDirectory) { $settingsPath = Join-Path -Path $MainScriptPath -ChildPath $ConfigDirectory if (! (Test-Path -Path $settingsPath -PathType Container)) { @@ -155,9 +156,7 @@ function Register-PodeService { } $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_srvsettings.json" - $binPath = Join-Path -path (Split-Path -Parent -Path $PSScriptRoot) -ChildPath 'Bin' - - # JSON content for the service settings + # Generate the service settings JSON file $jsonContent = @{ PodePwshWorker = @{ ScriptPath = $ScriptPath @@ -171,12 +170,12 @@ function Register-PodeService { } } - # Convert hash table to JSON and save it to the settings file + # 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 = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() - # Call the appropriate platform-specific function if ($IsWindows) { $param = @{ Name = $Name @@ -219,7 +218,7 @@ function Register-PodeService { $operation = Register-PodeMacService @param } - # Optionally start the service + # Optionally start the service if requested if ( $operation -and $Start.IsPresent) { $operation = Start-PodeService -Name $Name } @@ -260,14 +259,11 @@ function Register-PodeService { #> function Start-PodeService { param( + [Parameter(Mandatory = $true)] [string] $Name ) try { - if (! $Name) { - # Get the service name from the settings file - $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - } if ($IsWindows) { @@ -292,7 +288,8 @@ function Start-PodeService { } } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } } @@ -312,7 +309,8 @@ function Start-PodeService { } } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } } @@ -336,7 +334,8 @@ function Start-PodeService { } } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } } } @@ -373,14 +372,11 @@ function Start-PodeService { #> function Stop-PodeService { param( + [Parameter(Mandatory = $true)] [string] $Name ) try { - if (! $Name) { - # Get the service name from the settings file - $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - } if ($IsWindows) { if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { @@ -399,7 +395,8 @@ function Stop-PodeService { } } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } } elseif ($IsLinux) { @@ -415,7 +412,8 @@ function Stop-PodeService { } } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } } @@ -436,7 +434,8 @@ function Stop-PodeService { } } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } } } @@ -484,14 +483,11 @@ function Unregister-PodeService { [Parameter()] [switch]$Force, + [Parameter(Mandatory = $true)] [string] $Name ) - if (! $Name) { - # Get the service name from the settings file - $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - } if ($IsWindows) { if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow @@ -500,7 +496,8 @@ function Unregister-PodeService { # Check if the service exists $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if (-not $service) { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } try { @@ -511,7 +508,8 @@ function Unregister-PodeService { # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { - throw "Service '$Name' is running. Use the -Force parameter to forcefully stop." + # Service is running. Use the -Force parameter to forcefully stop." + throw ($Podelocale.serviceIsRunningException -f "pode.$Name") } } @@ -539,7 +537,8 @@ function Unregister-PodeService { # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { - throw "Service '$Name' is running. Use the -Force parameter to forcefully stop." + # Service is running. Use the -Force parameter to forcefully stop." + throw ($Podelocale.serviceIsRunningException -f "$Name.service") } } systemctl disable "$Name.service" @@ -547,7 +546,8 @@ function Unregister-PodeService { # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } return $true } @@ -581,7 +581,8 @@ function Unregister-PodeService { # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { - throw "Service '$Name' is running. Use the -Force parameter to forcefully stop." + # Service is running. Use the -Force parameter to forcefully stop." + throw ($Podelocale.serviceIsRunningException -f "$Name") } } launchctl unload ~/Library/LaunchAgents/pode.$Name.plist @@ -594,7 +595,8 @@ function Unregister-PodeService { # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } else { - throw "Service '$Name' is not registered." + # Service is not registered + throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") } return $true } @@ -649,15 +651,11 @@ function Unregister-PodeService { #> function Get-PodeService { param( + [Parameter(Mandatory = $true)] [string] $Name ) - if (! $Name) { - # Get the service name from the settings file - $Name = Get-PodeServiceName -Path (Split-Path -Path $MyInvocation.ScriptName -Parent) - } - if ($IsWindows) { if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow From 0091321c9df337221bb4a8a539869f4edbdb1c2d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 18 Oct 2024 09:48:32 -0700 Subject: [PATCH 09/93] Remove settingsfile with unregister --- .gitignore | 2 +- .../HelloService2_srvsettings.json | 12 ----- src/Public/Service.ps1 | 44 +++++++++++++++++-- 3 files changed, 42 insertions(+), 16 deletions(-) delete mode 100644 examples/HelloService/HelloService2_srvsettings.json diff --git a/.gitignore b/.gitignore index 81f5c3014..0c6a11c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -266,4 +266,4 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 docs/Getting-Started/Samples.md -examples/HelloService/HelloService_srvsettings.json +examples/*_srvsettings.json diff --git a/examples/HelloService/HelloService2_srvsettings.json b/examples/HelloService/HelloService2_srvsettings.json deleted file mode 100644 index ef95d025b..000000000 --- a/examples/HelloService/HelloService2_srvsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "PodePwshWorker": { - "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", - "Quiet": true, - "DisableTermination": true, - "ParameterString": "", - "Name": "HelloService2", - "ScriptPath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", - "ShutdownWaitTimeMs": 30000, - "LogFilePath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService2_svc.log" - } -} diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 34f8223f4..9821ccb22 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -176,6 +176,9 @@ function Register-PodeService { # Determine OS architecture and call platform-specific registration functions $osArchitecture = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() + # 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 ($IsWindows) { $param = @{ Name = $Name @@ -497,7 +500,7 @@ function Unregister-PodeService { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if (-not $service) { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceNotRegisteredException -f "$Name") } try { @@ -517,6 +520,13 @@ function Unregister-PodeService { Remove-Service -Name $Name -ErrorAction Stop # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + # Remove the service configuration + if ($service.BinaryPathName) { + $binaryPath = $service.BinaryPathName.trim('"').split('" "') + if ((Test-Path -Path ($binaryPath[1]) -PathType Leaf)) { + Remove-Item -Path ($binaryPath[1]) -ErrorAction Break + } + } return $true } catch { @@ -542,7 +552,18 @@ function Unregister-PodeService { } } systemctl disable "$Name.service" - Remove-Item "/etc/systemd/system/$Name.service" + + # Read the content of the service file + $serviceFilePath = "/etc/systemd/system/$Name.service" + $serviceFileContent = Get-Content -Path $serviceFilePath + + # Extract the SettingsFile from the ExecStart line using regex + $settingsFile = $serviceFileContent | Select-String -Pattern 'ExecStart=.*\s+(.*)' | ForEach-Object { $_.Matches[0].Groups[1].Value } + if ((Test-Path -Path $settingsFile -PathType Leaf)) { + Remove-Item -Path $settingsFile + } + + Remove-Item -Path $serviceFilePath -ErrorAction Break # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } else { @@ -587,7 +608,24 @@ function Unregister-PodeService { } launchctl unload ~/Library/LaunchAgents/pode.$Name.plist if ($LASTEXITCODE -eq 0) { - Remove-Item "~/Library/LaunchAgents/pode.$Name.plist" -ErrorAction Break + + $plistFilePath = "~/Library/LaunchAgents/pode.$Name.plist" + + # Read the content of the plist file + $plistFileContent = Get-Content -Path $plistFilePath + + # Extract the SettingsFile from the ProgramArguments array using regex + $settingsFile = $plistFileContent | Select-String -Pattern '(.*)' | ForEach-Object { + if ($_.Line -match 'PodeMonitor.*(.*)') { + $matches[1] + } + } + + if ((Test-Path -Path $settingsFile -PathType Leaf)) { + Remove-Item -Path $settingsFile + } + + Remove-Item -Path $plistFilePath -ErrorAction Break } else { return $false From e29752eaab9a224d948a66e177ac8c97a2e1a803 Mon Sep 17 00:00:00 2001 From: Max Daneri Date: Fri, 18 Oct 2024 15:58:52 -0400 Subject: [PATCH 10/93] fixes --- .../HelloService2_srvsettings.json | 12 +++++++++++ src/Private/Service.ps1 | 21 ++++++++++++------- src/Public/Service.ps1 | 20 +++++++++--------- 3 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 examples/HelloService/HelloService2_srvsettings.json diff --git a/examples/HelloService/HelloService2_srvsettings.json b/examples/HelloService/HelloService2_srvsettings.json new file mode 100644 index 000000000..67d1d7f98 --- /dev/null +++ b/examples/HelloService/HelloService2_srvsettings.json @@ -0,0 +1,12 @@ +{ + "PodePwshWorker": { + "DisableTermination": true, + "Quiet": true, + "Name": "HelloService2", + "ParameterString": "", + "ShutdownWaitTimeMs": 30000, + "LogFilePath": "/home/m.daneri/Documents/Pode/examples/HelloService/logs/HelloService2_svc.log", + "PwshPath": "/opt/microsoft/powershell/7/pwsh", + "ScriptPath": "/home/m.daneri/Documents/Pode/examples/HelloService/HelloService.ps1" + } +} diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index ae2eb7da0..512e1b3d1 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -197,6 +197,8 @@ function Register-PodeMacService { # 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 @" @@ -231,11 +233,13 @@ function Register-PodeMacService { -"@ | Set-Content -Path "~/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 +"@ | Set-Content -Path $tempFile -Encoding UTF8 + + sudo cp $tempFile "~/Library/LaunchAgents/pode.$($Name).plist" try { # Load the plist with launchctl - launchctl load ~/Library/LaunchAgents/pode.$($Name).plist + sudo launchctl load ~/Library/LaunchAgents/pode.$($Name).plist # Verify the service is now registered if (-not (launchctl list | Select-String "pode.$Name")) { @@ -346,7 +350,8 @@ function Register-PodeLinuxService { # Service is already registered. throw ($PodeLocale.serviceAlreadyRegisteredException -f "$Name.service" ) } - + # Create a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() # Create the service file @" [Unit] @@ -354,7 +359,7 @@ Description=$Description After=network.target [Service] -ExecStart=$BinPath/linux-$OsArchitecture/PodeMonitor $SettingsFile +ExecStart=$BinPath/$OsArchitecture/PodeMonitor $SettingsFile WorkingDirectory=$BinPath Restart=always User=$User @@ -364,7 +369,9 @@ Group=$Group [Install] WantedBy=multi-user.target -"@ | Set-Content -Path "/etc/systemd/system/$($Name).service" -Encoding UTF8 +"@ | Set-Content -Path $tempFile -Encoding UTF8 + + sudo cp $tempFile "/etc/systemd/system/$($Name).service" # Create user if needed if (!$SkipUserCreation.IsPresent) { @@ -372,13 +379,13 @@ WantedBy=multi-user.target id $User 2>&1 if ($LASTEXITCODE -ne 0) { # Create the user if it doesn't exist - useradd -r -s /bin/false $User + sudo useradd -r -s /bin/false $User } } # Enable the service and check if it fails try { - systemctl enable "$Name.service" + sudo systemctl enable "$Name.service" if ($LASTEXITCODE -ne 0) { # Service registration failed. throw ($PodeLocale.serviceRegistrationFailedException -f "$Name.service") diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 9821ccb22..b8c120e27 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -302,7 +302,7 @@ function Start-PodeService { # Check if the service is already running $status = systemctl is-active "$Name.service" if ($status -ne 'active') { - systemctl start "$Name.service" + sudo systemctl start "$Name.service" # Log service started successfully # Write-PodeServiceLog -Message "Service '$Name' started successfully." } @@ -325,7 +325,7 @@ function Start-PodeService { # Check if the service has a PID entry if (!($serviceInfo -match '"PID" = (\d+);')) { - launchctl start "pode.$Name" + sudo launchctl start "pode.$Name" # Log service started successfully # Write-PodeServiceLog -Message "Service '$Name' started successfully." @@ -407,7 +407,7 @@ function Stop-PodeService { if (systemctl status "$Name.service" -q) { $status = systemctl is-active "$Name.service" if ($status -eq 'active') { - systemctl stop "$Name.service" + sudo systemctl stop "$Name.service" # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." } else { @@ -428,7 +428,7 @@ function Stop-PodeService { # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { - launchctl stop "pode.$Name" + sudo launchctl stop "pode.$Name" # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." return ($LASTEXITCODE -eq 0) } @@ -543,7 +543,7 @@ function Unregister-PodeService { $status = systemctl is-active "$Name.service" if ($status -eq 'active') { if ($Force.IsPresent) { - systemctl stop "$Name.service" + sudo systemctl stop "$Name.service" # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { @@ -551,7 +551,7 @@ function Unregister-PodeService { throw ($Podelocale.serviceIsRunningException -f "$Name.service") } } - systemctl disable "$Name.service" + sudo systemctl disable "$Name.service" # Read the content of the service file $serviceFilePath = "/etc/systemd/system/$Name.service" @@ -586,7 +586,7 @@ function Unregister-PodeService { $serviceInfo = launchctl list "pode.$Name" -join "`n" # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { - launchctl stop "pode.$Name" + sudo launchctl stop "pode.$Name" # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." $serviceIsRunning = ($LASTEXITCODE -ne 0) } @@ -598,7 +598,7 @@ function Unregister-PodeService { # Check if the service is running if ( $serviceIsRunning) { if ($Force.IsPresent) { - launchctl stop "pode.$Name" + sudo launchctl stop "pode.$Name" # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { @@ -606,7 +606,7 @@ function Unregister-PodeService { throw ($Podelocale.serviceIsRunningException -f "$Name") } } - launchctl unload ~/Library/LaunchAgents/pode.$Name.plist + sudo launchctl unload ~/Library/LaunchAgents/pode.$Name.plist if ($LASTEXITCODE -eq 0) { $plistFilePath = "~/Library/LaunchAgents/pode.$Name.plist" @@ -617,7 +617,7 @@ function Unregister-PodeService { # Extract the SettingsFile from the ProgramArguments array using regex $settingsFile = $plistFileContent | Select-String -Pattern '(.*)' | ForEach-Object { if ($_.Line -match 'PodeMonitor.*(.*)') { - $matches[1] + $matches[1] } } From 9d90c78f751b73e15b0a7bb779fd1e9607d29306 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 19 Oct 2024 09:23:11 -0700 Subject: [PATCH 11/93] Add UAC support --- examples/HelloService/HelloService.ps1 | 77 ++++++++-- src/Locales/ar/Pode.psd1 | 7 +- src/Locales/de/Pode.psd1 | 7 +- src/Locales/en-us/Pode.psd1 | 7 +- src/Locales/en/Pode.psd1 | 7 +- src/Locales/es/Pode.psd1 | 7 +- src/Locales/fr/Pode.psd1 | 7 +- src/Locales/it/Pode.psd1 | 7 +- src/Locales/ja/Pode.psd1 | 7 +- src/Locales/ko/Pode.psd1 | 7 +- src/Locales/nl/Pode.psd1 | 7 +- src/Locales/pl/Pode.psd1 | 7 +- src/Locales/pt/Pode.psd1 | 7 +- src/Locales/zh/Pode.psd1 | 7 +- src/Private/Helpers.ps1 | 190 +++++++++++++++++++------ src/Private/Service.ps1 | 47 +++++- src/Public/Core.ps1 | 2 +- src/Public/Service.ps1 | 108 ++++++++------ tests/unit/Context.Tests.ps1 | 6 +- tests/unit/Routes.Tests.ps1 | 2 +- 20 files changed, 372 insertions(+), 151 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index c477d76f9..3ec90abf6 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -1,34 +1,83 @@ <# .SYNOPSIS - PowerShell script to set up a Pode server with a simple GET endpoint. + PowerShell script to register, start, stop, query, and unregister a Pode service, with a basic server setup. .DESCRIPTION - This script sets up a Pode server that listens on port 8080. It includes a single route for GET requests - to the root path ('/') that returns a simple text response. + This script manages a Pode service named 'Hello Service' with commands to register, start, stop, query, + and unregister the service. Additionally, it sets up a Pode server that listens on port 8080 and includes + a simple 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. + +.PARAMETER Register + Registers the 'Hello Service' with Pode. + +.PARAMETER Unregister + Unregisters the 'Hello Service' 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 'Hello Service'. + +.PARAMETER Stop + Stops the 'Hello Service'. + +.PARAMETER Query + Queries the status of the 'Hello Service'. .EXAMPLE - To run the sample: ./HelloWorld/HelloWorld.ps1 + Register the service: + ./HelloWorld.ps1 -Register - # HTML responses 'Hello, world! - Invoke-RestMethod -Uri http://localhost:8081/ -Method Get +.EXAMPLE + Start the service: + ./HelloWorld.ps1 -Start + +.EXAMPLE + Query the service: + ./HelloWorld.ps1 -Query + +.EXAMPLE + Stop the service: + ./HelloWorld.ps1 -Stop + +.EXAMPLE + Unregister the service: + ./HelloWorld.ps1 -Unregister -Force .LINK - https://github.com/Badgerati/Pode/blob/develop/examples/HelloWorld/HelloWorld.ps1 + https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloService.ps1 .NOTES + Test the Pode server's HTTP endpoint: + Invoke-RestMethod -Uri http://localhost:8080/ -Method Get + # Response: 'Hello, Service!' + + Author: Pode Team License: MIT License #> +[CmdletBinding(DefaultParameterSetName = 'Inbuilt')] param( + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] [switch] $Register, + [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 ) @@ -54,26 +103,26 @@ catch { } -if ($Register.IsPresent) { - Register-PodeService -Name 'HelloService2' -Start +if ( $Register.IsPresent) { + Register-PodeService -Name 'Hello Service' exit } -if ($Unregister.IsPresent) { - Unregister-PodeService -Name 'HelloService2' +if ( $Unregister.IsPresent) { + Unregister-PodeService -Name 'Hello Service' -Force:$Force exit } if ($Start.IsPresent) { - Start-PodeService -Name 'HelloService2' + Start-PodeService -Name 'Hello Service' exit } if ($Stop.IsPresent) { - Stop-PodeService -Name 'HelloService2' + Stop-PodeService -Name 'Hello Service' exit } if ($Query.IsPresent) { - Get-PodeService -Name 'HelloService2' + Get-PodeService -Name 'Hello Service' exit } # Alternatively, you can directly import the Pode module from the system diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 76bff313a..14bdc698a 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' serviceAlreadyRegisteredException = "الخدمة '{0}' مسجلة بالفعل." - serviceNotRegisteredException = "الخدمة '{0}' غير مسجلة." - serviceRegistrationException = "الخدمة '{0}' غير مسجلة." - serviceRegistrationFailedException = "فشل تسجيل الخدمة '{0}'." + serviceIsNotRegisteredException = "الخدمة '{0}' غير مسجلة." + serviceCommandFailedException = "فشل الأمر '{0}' في الخدمة '{1}'." + serviceRegistrationException = "فشل تسجيل الخدمة '{0}'." serviceIsRunningException = "الخدمة '{0}' تعمل. استخدم المعامل -Force للإيقاف بالقوة." + serviceUnRegistrationException = "فشل إلغاء تسجيل الخدمة '{0}'." } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index 1eb6d95ea..4582cd7b0 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe." unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}' serviceAlreadyRegisteredException = "Der Dienst '{0}' ist bereits registriert." - serviceNotRegisteredException = "Der Dienst '{0}' ist nicht registriert." - serviceRegistrationException = "Der Dienst '{0}' ist nicht registriert." - serviceRegistrationFailedException = "Die Registrierung des Dienstes '{0}' ist fehlgeschlagen." + 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." } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index e933f9124..5f18976e6 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' serviceAlreadyRegisteredException = "Service '{0}' is already registered." - serviceNotRegisteredException = "Service '{0}' is not registered." - serviceRegistrationException = "Service '{0}' is not registered." - serviceRegistrationFailedException = "Service '{0}' registration failed." + 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." } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index d795170c8..8afc7975e 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' serviceAlreadyRegisteredException = "Service '{0}' is already registered." - serviceNotRegisteredException = "Service '{0}' is not registered." - serviceRegistrationException = "Service '{0}' is not registered." - serviceRegistrationFailedException = "Service '{0}' registration failed." + 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." } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index d46948e46..e719be7d3 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -292,8 +292,9 @@ 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}' serviceAlreadyRegisteredException = "El servicio '{0}' ya está registrado." - serviceNotRegisteredException = "El servicio '{0}' no está registrado." - serviceRegistrationException = "El servicio '{0}' no está registrado." - serviceRegistrationFailedException = "Falló el registro del servicio '{0}'." + 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ó." } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index fa8b02f7b..5e0af10df 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -292,9 +292,10 @@ 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." serviceAlreadyRegisteredException = "Le service '{0}' est déjà enregistré." - serviceNotRegisteredException = "Le service '{0}' n'est pas enregistré." - serviceRegistrationException = "Le service '{0}' n'est pas enregistré." - serviceRegistrationFailedException = "Échec de l'enregistrement du service '{0}'." + 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é." } diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 8b9f966bc..3f095dd98 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non è supportata per la codifica {0}' serviceAlreadyRegisteredException = "Il servizio '{0}' è già registrato." - serviceNotRegisteredException = "Il servizio '{0}' non è registrato." - serviceRegistrationException = "Il servizio '{0}' non è registrato." - serviceRegistrationFailedException = "Registrazione del servizio '{0}' non riuscita." + 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." } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 22c78f4c8..931761d88 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。" unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' serviceAlreadyRegisteredException = "サービス '{0}' はすでに登録されています。" - serviceNotRegisteredException = "サービス '{0}' は登録されていません。" - serviceRegistrationException = "サービス '{0}' は登録されていません。" - serviceRegistrationFailedException = "サービス '{0}' の登録に失敗しました。" + serviceIsNotRegisteredException = "サービス '{0}' は登録されていません。" + serviceCommandFailedException = "サービスコマンド '{0}' はサービス '{1}' で失敗しました。" + serviceRegistrationException = "サービス '{0}' の登録に失敗しました。" serviceIsRunningException = "サービス '{0}' が実行中です。強制的に停止するには、-Force パラメーターを使用してください。" + serviceUnRegistrationException = "サービス '{0}' の登録解除に失敗しました。" } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 5d0c475d1..507564f06 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' serviceAlreadyRegisteredException = "서비스 '{0}'가 이미 등록되었습니다." - serviceNotRegisteredException = "서비스 '{0}'가 등록되지 않았습니다." - serviceRegistrationException = "서비스 '{0}'가 등록되지 않았습니다." - serviceRegistrationFailedException = "서비스 '{0}' 등록에 실패했습니다." + serviceIsNotRegisteredException = "서비스 '{0}'가 등록되지 않았습니다." + serviceCommandFailedException = "서비스 명령 '{0}' 이(가) 서비스 '{1}' 에서 실패했습니다." + serviceRegistrationException = "서비스 '{0}' 등록에 실패했습니다." serviceIsRunningException = "서비스 '{0}'가 실행 중입니다. 강제로 중지하려면 -Force 매개변수를 사용하세요." + serviceUnRegistrationException = "서비스 '{0}' 등록 취소에 실패했습니다." } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index f96b6ea0a..2f682e47f 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer." unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}' serviceAlreadyRegisteredException = "De service '{0}' is al geregistreerd." - serviceNotRegisteredException = "De service '{0}' is niet geregistreerd." - serviceRegistrationException = "De service '{0}' is niet geregistreerd." - serviceRegistrationFailedException = "Registratie van de service '{0}' is mislukt." + 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." } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index ca9c11cde..f00bfbe9e 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku." unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}' serviceAlreadyRegisteredException = "Usługa '{0}' jest już zarejestrowana." - serviceNotRegisteredException = "Usługa '{0}' nie jest zarejestrowana." - serviceRegistrationException = "Usługa '{0}' nie jest zarejestrowana." - serviceRegistrationFailedException = "Rejestracja usługi '{0}' nie powiodła się." + 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}'." } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index 7dfc10bd2..8fec4cd45 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -292,8 +292,9 @@ 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.' serviceAlreadyRegisteredException = "O serviço '{0}' já está registrado." - serviceNotRegisteredException = "O serviço '{0}' não está registrado." - serviceRegistrationException = "O serviço '{0}' não está registrado." - serviceRegistrationFailedException = "Falha no registro do serviço '{0}'." + 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." } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 0ac6373c3..39b40d353 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -292,8 +292,9 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' serviceAlreadyRegisteredException = "服务 '{0}' 已经注册。" - serviceNotRegisteredException = "服务 '{0}' 未注册。" - serviceRegistrationException = "服务 '{0}' 未注册。" - serviceRegistrationFailedException = "服务 '{0}' 注册失败。" + serviceIsNotRegisteredException = "服务 '{0}' 未注册。" + serviceCommandFailedException = "服务命令 '{0}' 在服务 '{1}' 上失败。" + serviceRegistrationException = "服务 '{0}' 注册失败。" serviceIsRunningException = "服务 '{0}' 正在运行。使用 -Force 参数强制停止。" + serviceUnRegistrationException = "服务 '{0}' 的注销失败。" } \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 5bdf017bd..2e3362e93 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)] @@ -3776,46 +3755,169 @@ function Copy-PodeObjectDeepClone { return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) } } +<# +.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. +#> + +function Test-PodeAdminPrivilege { + param( + [switch] + $Elevate, + [switch] + $Console + ) + try { + # Check if the operating system is Windows + if ($IsWindows) { + $principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()) + if ($null -eq $principal) { + return $false + } + + $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if ($isAdmin) { + 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) + + # macOS typically uses 'admin' group for sudo privileges + return ($groups -match '\bsudo\b' -or $groups -match '\badmin\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 - Checks if the current user has administrative privileges on Windows. + Starts a command with elevated privileges if the current session is not already elevated. .DESCRIPTION - The `Test-PodeIsAdmin` function verifies if the current user has the necessary - privileges to perform administrative tasks by checking if they belong to the - Windows Administrator role. It will only run on Windows and returns `$true` if - the user has administrative privileges, otherwise `$false`. + 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. - If executed on a non-Windows platform, it returns `$false` and displays a message - indicating that the function is only applicable to Windows. +.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 - PS> Test-PodeIsAdmin - True + Invoke-PodeWinElevatedCommand -Command "Get-Service" -Arguments "-Name 'W32Time'" - This command checks if the current user is an administrator on a Windows system. + This will run the `Get-Service` command with elevated privileges, pass the `-Name 'W32Time'` argument, and return the result. .EXAMPLE - PS> if (Test-PodeIsAdmin) { "User has admin rights" } else { "User does not have admin rights" } + Invoke-PodeWinElevatedCommand -Command "C:\Scripts\MyScript.ps1" -Arguments "-Param1 'Value1' -Param2 'Value2'" - This command conditionally outputs whether the current user has administrative rights - on Windows. If the script is run on a non-Windows system, it outputs "User does not - have admin rights." + This will run the script `MyScript.ps1` with elevated privileges, pass the parameters `-Param1` and `-Param2`, and return the result. .NOTES - This function will only check for administrative privileges if executed on a Windows system. + This function is particularly useful when running commands or scripts that require administrator rights. #> -function Test-PodeIsAdmin { - # Check if the operating system is Windows - if ($IsWindows -ne $true) { - return $false +function Invoke-PodeWinElevatedCommand { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] + param ( + [string] + $Command, + [string] + $Arguments + ) + + # 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 '"', '"""' + + # Combine command and arguments into a string for elevated execution + $escapedCommand = "`"$Command`" $escapedArguments" + # Combine command and arguments into a string to pass for elevated execution + # $escapedCommand = "`"$Command`" $Arguments" + + # 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 } - $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = [Security.Principal.WindowsPrincipal]::new($currentUser) - return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + # Run the command directly with arguments if elevated and capture the output + return Invoke-Expression "$Command $Arguments" } - \ No newline at end of file diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 512e1b3d1..b7c9a32f8 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -244,7 +244,7 @@ function Register-PodeMacService { # Verify the service is now registered if (-not (launchctl list | Select-String "pode.$Name")) { # Service registration failed. - throw ($PodeLocale.serviceRegistrationFailedException -f "pode.$Name") + throw ($PodeLocale.serviceRegistrationException -f "pode.$Name") } } @@ -388,7 +388,7 @@ WantedBy=multi-user.target sudo systemctl enable "$Name.service" if ($LASTEXITCODE -ne 0) { # Service registration failed. - throw ($PodeLocale.serviceRegistrationFailedException -f "$Name.service") + throw ($PodeLocale.serviceRegistrationException -f "$Name.service") } } catch { @@ -515,10 +515,12 @@ function Register-PodeWindowsService { } try { - $sv = New-Service @params -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $paramsString = $params.GetEnumerator() | ForEach-Object { "-$($_.Key) '$($_.Value)'" } + $sv = Invoke-PodeWinElevatedCommand -Command 'New-Service' -Arguments ($paramsString -join ' ') + if (!$sv) { # Service registration failed. - throw ($PodeLocale.serviceRegistrationFailedException -f "$Name") + throw ($PodeLocale.serviceRegistrationException -f "$Name") } } catch { @@ -639,4 +641,41 @@ function Test-PodeUserServiceCreationPrivilege { 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 ($IsWindows -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 + } } \ No newline at end of file diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index a2504976c..b5bd3f03d 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -1105,7 +1105,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/Service.ps1 b/src/Public/Service.ps1 index b8c120e27..ac9507694 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -115,15 +115,12 @@ function Register-PodeService { [string] $ConfigDirectory ) - try { - # Check for administrative privileges on Windows - if ($IsWindows) { - if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { - Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow - exit - } - } + # 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 @@ -266,24 +263,28 @@ function Start-PodeService { [string] $Name ) + # 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 { if ($IsWindows) { - # Check if the current script is running as Administrator - if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { - Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow - exit - } - # Get the Windows service $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is already running if ($service.Status -ne 'Running') { - Start-Service -Name $Name -ErrorAction Stop - # Log service started successfully - # Write-PodeServiceLog -Message "Service '$Name' started successfully." + $null = Invoke-PodeWinElevatedCommand -Command 'Start-Service' -Arguments "-Name '$Name'" + + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service.Status -eq 'Running') { + # Write-PodeServiceLog -Message "Service '$Name' started successfully." + } + else { + throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $Name) + } } else { # Log service is already running @@ -292,7 +293,7 @@ function Start-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } } @@ -313,7 +314,7 @@ function Start-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } } @@ -338,7 +339,7 @@ function Start-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } } } @@ -380,18 +381,24 @@ function Stop-PodeService { $Name ) try { + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege if ($IsWindows) { - if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { - Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow - exit - } + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running if ($service.Status -eq 'Running') { - Stop-Service -Name $Name -ErrorAction Stop -WarningAction SilentlyContinue - # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service.Status -eq 'Stopped') { + # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + } + else { + throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) + } } else { # Write-PodeServiceLog -Message "Service '$Name' is not running." @@ -399,7 +406,7 @@ function Stop-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } } elseif ($IsLinux) { @@ -416,7 +423,7 @@ function Stop-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } } @@ -438,7 +445,7 @@ function Stop-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } } } @@ -490,25 +497,31 @@ function Unregister-PodeService { [string] $Name ) + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege if ($IsWindows) { - if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { - Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow - exit - } # Check if the service exists $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if (-not $service) { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "$Name") } try { + $pathName=$service.BinaryPathName # Check if the service is running before attempting to stop it if ($service.Status -eq 'Running') { if ($Force.IsPresent) { - Stop-Service -Name $Name -Force -ErrorAction Stop - # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service.Status -eq 'Stopped') { + # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + } + else { + throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) + } } else { # Service is running. Use the -Force parameter to forcefully stop." @@ -517,17 +530,23 @@ function Unregister-PodeService { } # Remove the service - Remove-Service -Name $Name -ErrorAction Stop + $null = Invoke-PodeWinElevatedCommand -Command 'Remove-Service' -Arguments "-Name '$Name'" + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($null -ne $service) { + # Write-PodeServiceLog -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) + } # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." # Remove the service configuration - if ($service.BinaryPathName) { - $binaryPath = $service.BinaryPathName.trim('"').split('" "') + if ($pathName) { + $binaryPath = $pathName.trim('"').split('" "') if ((Test-Path -Path ($binaryPath[1]) -PathType Leaf)) { Remove-Item -Path ($binaryPath[1]) -ErrorAction Break } } return $true + } catch { $_ | Write-PodeErrorLog @@ -568,7 +587,7 @@ function Unregister-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } return $true } @@ -634,7 +653,7 @@ function Unregister-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") } return $true } @@ -693,12 +712,11 @@ function Get-PodeService { [string] $Name ) + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege if ($IsWindows) { - if (! (Test-PodeIsAdmin) -and ! (Test-PodeUserServiceCreationPrivilege) ) { - Write-PodeHost "This script needs to run as Administrator or with the 'SERVICE_CHANGE_CONFIG'(SeCreateServicePrivilege) privilege." -ForegroundColor Yellow - exit - } # Check if the service exists on Windows $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 6006e6428..4e5c9c529 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 User { 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 User { 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' = @{ From 2ca3621540471c8349e63fb918631a71d303b48d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 19 Oct 2024 10:57:50 -0700 Subject: [PATCH 12/93] FIx Mac service --- examples/HelloService/HelloService.ps1 | 9 +++---- src/Private/Service.ps1 | 12 ++++----- src/Public/Service.ps1 | 36 ++++++++++++-------------- tests/unit/Context.Tests.ps1 | 4 +-- 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 3ec90abf6..21608d970 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -9,6 +9,10 @@ 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 Register Registers the 'Hello Service' with Pode. @@ -51,11 +55,6 @@ https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloService.ps1 .NOTES - Test the Pode server's HTTP endpoint: - Invoke-RestMethod -Uri http://localhost:8080/ -Method Get - # Response: 'Hello, Service!' - - Author: Pode Team License: MIT License #> diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index b7c9a32f8..4faa1a3bf 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -196,9 +196,7 @@ function Register-PodeMacService { # 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 @" @@ -233,16 +231,16 @@ function Register-PodeMacService { -"@ | Set-Content -Path $tempFile -Encoding UTF8 +"@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 - sudo cp $tempFile "~/Library/LaunchAgents/pode.$($Name).plist" + chmod +r "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" try { # Load the plist with launchctl - sudo launchctl load ~/Library/LaunchAgents/pode.$($Name).plist + launchctl load "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" # Verify the service is now registered - if (-not (launchctl list | Select-String "pode.$Name")) { + if (! (launchctl list | Select-String "pode.$Name")) { # Service registration failed. throw ($PodeLocale.serviceRegistrationException -f "pode.$Name") diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index ac9507694..70f78b7a2 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -326,7 +326,7 @@ function Start-PodeService { # Check if the service has a PID entry if (!($serviceInfo -match '"PID" = (\d+);')) { - sudo launchctl start "pode.$Name" + launchctl start "pode.$Name" # Log service started successfully # Write-PodeServiceLog -Message "Service '$Name' started successfully." @@ -435,7 +435,7 @@ function Stop-PodeService { # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { - sudo launchctl stop "pode.$Name" + launchctl stop "pode.$Name" # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." return ($LASTEXITCODE -eq 0) } @@ -510,7 +510,7 @@ function Unregister-PodeService { } try { - $pathName=$service.BinaryPathName + $pathName = $service.BinaryPathName # Check if the service is running before attempting to stop it if ($service.Status -eq 'Running') { if ($Force.IsPresent) { @@ -602,10 +602,10 @@ function Unregister-PodeService { # Check if the service exists if (launchctl list | Select-String "pode.$Name") { - $serviceInfo = launchctl list "pode.$Name" -join "`n" + $serviceInfo = (launchctl list "pode.$Name") -join "`n" # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { - sudo launchctl stop "pode.$Name" + launchctl stop "pode.$Name" # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." $serviceIsRunning = ($LASTEXITCODE -ne 0) } @@ -617,7 +617,7 @@ function Unregister-PodeService { # Check if the service is running if ( $serviceIsRunning) { if ($Force.IsPresent) { - sudo launchctl stop "pode.$Name" + launchctl stop "pode.$Name" # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { @@ -625,26 +625,24 @@ function Unregister-PodeService { throw ($Podelocale.serviceIsRunningException -f "$Name") } } - sudo launchctl unload ~/Library/LaunchAgents/pode.$Name.plist + launchctl unload "$HOME/Library/LaunchAgents/pode.$Name.plist" if ($LASTEXITCODE -eq 0) { - $plistFilePath = "~/Library/LaunchAgents/pode.$Name.plist" + $plistFilePath = "$HOME/Library/LaunchAgents/pode.$Name.plist" + #Check if the plist file exists + if (Test-Path -Path $plistFilePath) { + # Read the content of the plist file + $plistXml = [xml](Get-Content -Path $plistFilePath -Raw) - # Read the content of the plist file - $plistFileContent = Get-Content -Path $plistFilePath + # Extract the second string in the ProgramArguments array (the settings file path) + $settingsFile = $plistXml.plist.dict.array.string[1] - # Extract the SettingsFile from the ProgramArguments array using regex - $settingsFile = $plistFileContent | Select-String -Pattern '(.*)' | ForEach-Object { - if ($_.Line -match 'PodeMonitor.*(.*)') { - $matches[1] + if ((Test-Path -Path $settingsFile -PathType Leaf)) { + Remove-Item -Path $settingsFile } - } - if ((Test-Path -Path $settingsFile -PathType Leaf)) { - Remove-Item -Path $settingsFile + Remove-Item -Path $plistFilePath -ErrorAction Break } - - Remove-Item -Path $plistFilePath -ErrorAction Break } else { return $false diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 4e5c9c529..6750f20e6 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -371,7 +371,7 @@ Describe 'Add-PodeEndpoint' { } It 'Throws an error for not running as admin' { - Mock Test-PodeAdminPrivilege User { 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-PodeAdminPrivilege User { return $true } } + Mock Test-PodeAdminPrivilege { return $true } } It 'Returns no Endpoints' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } From 1f55857a2c0fff286cebff41b990ea5d7f3d152a Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 21 Oct 2024 08:03:04 -0700 Subject: [PATCH 13/93] Update Pode.psd1 --- src/Locales/en-us/Pode.psd1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index d1fd4e1e0..f856d8d2c 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -291,10 +291,11 @@ getRequestBodyNotAllowedExceptionMessage = '{0} operations cannot have a Request Body.' 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." + 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." + serviceRegistrationException = "Service '{0}' registration failed." serviceIsRunningException = "Service '{0}' is running. Use the -Force parameter to forcefully stop." serviceUnRegistrationException = "Service '{0}' unregistration failed." } \ No newline at end of file From 97d8e3c2eeeb94ddffcde7027fbfea58e31aaf2b Mon Sep 17 00:00:00 2001 From: Max Daneri Date: Mon, 21 Oct 2024 15:12:14 -0400 Subject: [PATCH 14/93] fix linux 1 --- .../HelloService/Hello Service_srvsettings.json | 12 ++++++++++++ src/Private/Helpers.ps1 | 14 +++++++------- src/Private/Service.ps1 | 14 ++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 examples/HelloService/Hello Service_srvsettings.json diff --git a/examples/HelloService/Hello Service_srvsettings.json b/examples/HelloService/Hello Service_srvsettings.json new file mode 100644 index 000000000..daa644657 --- /dev/null +++ b/examples/HelloService/Hello Service_srvsettings.json @@ -0,0 +1,12 @@ +{ + "PodePwshWorker": { + "PwshPath": "/opt/microsoft/powershell/7/pwsh", + "LogFilePath": "/home/m.daneri/Documents/Pode/examples/HelloService/logs/Hello Service_svc.log", + "ParameterString": "", + "ScriptPath": "/home/m.daneri/Documents/Pode/examples/HelloService/HelloService.ps1", + "DisableTermination": true, + "Quiet": true, + "Name": "Hello Service", + "ShutdownWaitTimeMs": 30000 + } +} diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index fa4098962..e93c02bef 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3844,7 +3844,7 @@ function Test-PodeAdminPrivilege { $groups = (groups $user) # macOS typically uses 'admin' group for sudo privileges - return ($groups -match '\bsudo\b' -or $groups -match '\badmin\b') + return ($groups -match '\bwheel\b' -or $groups -match '\badmin\b') } return $false } @@ -3901,16 +3901,16 @@ function Invoke-PodeWinElevatedCommand { $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 '"', '"""' + # Escape the arguments by replacing " with `" (escaping quotes) + $escapedArguments = $Arguments -replace '"', '"""' - # Combine command and arguments into a string for elevated execution - $escapedCommand = "`"$Command`" $escapedArguments" + # Combine command and arguments into a string for elevated execution + $escapedCommand = "`"$Command`" $escapedArguments" # Combine command and arguments into a string to pass for elevated execution - # $escapedCommand = "`"$Command`" $Arguments" + # $escapedCommand = "`"$Command`" $Arguments" # Start elevated process with properly escaped command and arguments - $result= Start-Process -FilePath ((Get-Process -Id $PID).Path) ` + $result = Start-Process -FilePath ((Get-Process -Id $PID).Path) ` -ArgumentList '-NoProfile', '-ExecutionPolicy Bypass', "-Command & {$escapedCommand}" ` -Verb RunAs -Wait -PassThru diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 4faa1a3bf..fed99e91d 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -196,7 +196,7 @@ function Register-PodeMacService { # Determine whether the service should run at load $runAtLoad = if ($Autostart.IsPresent) { '' } else { '' } - + # Create the plist content @" @@ -342,11 +342,13 @@ function Register-PodeLinuxService { [string] $OsArchitecture ) + $nameService = "$Name.service".Replace(' ', '\x20') + $output = bash -c "systemctl status $nameService 2>&1" # Check if the service is already registered - if (systemctl status "$Name.service" -ErrorAction SilentlyContinue) { + if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { # Service is already registered. - throw ($PodeLocale.serviceAlreadyRegisteredException -f "$Name.service" ) + throw ($PodeLocale.serviceAlreadyRegisteredException -f $nameService ) } # Create a temporary file $tempFile = [System.IO.Path]::GetTempFileName() @@ -369,7 +371,7 @@ Group=$Group WantedBy=multi-user.target "@ | Set-Content -Path $tempFile -Encoding UTF8 - sudo cp $tempFile "/etc/systemd/system/$($Name).service" + sudo cp $tempFile "/etc/systemd/system/$nameService" # Create user if needed if (!$SkipUserCreation.IsPresent) { @@ -383,10 +385,10 @@ WantedBy=multi-user.target # Enable the service and check if it fails try { - sudo systemctl enable "$Name.service" + sudo systemctl enable $nameService if ($LASTEXITCODE -ne 0) { # Service registration failed. - throw ($PodeLocale.serviceRegistrationException -f "$Name.service") + throw ($PodeLocale.serviceRegistrationException -f $nameService) } } catch { From 734f6b34d07505f2f53a0fa72a17946eca3720c2 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 21 Oct 2024 13:06:19 -0700 Subject: [PATCH 15/93] fixes --- .../HelloService_srvsettings.json | 12 +++ src/Private/Service.ps1 | 6 +- src/Public/Service.ps1 | 101 +++++++++++------- 3 files changed, 77 insertions(+), 42 deletions(-) create mode 100644 examples/HelloService/HelloService_srvsettings.json diff --git a/examples/HelloService/HelloService_srvsettings.json b/examples/HelloService/HelloService_srvsettings.json new file mode 100644 index 000000000..647d9f32b --- /dev/null +++ b/examples/HelloService/HelloService_srvsettings.json @@ -0,0 +1,12 @@ +{ + "PodePwshWorker": { + "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + "Quiet": true, + "DisableTermination": true, + "ParameterString": "", + "Name": "HelloService", + "ScriptPath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", + "ShutdownWaitTimeMs": 30000, + "LogFilePath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService_svc.log" + } +} diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index fed99e91d..faab9c5e6 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -343,7 +343,7 @@ function Register-PodeLinuxService { $OsArchitecture ) $nameService = "$Name.service".Replace(' ', '\x20') - $output = bash -c "systemctl status $nameService 2>&1" + systemctl status $nameService 2>&1 # Check if the service is already registered if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { @@ -385,7 +385,7 @@ WantedBy=multi-user.target # Enable the service and check if it fails try { - sudo systemctl enable $nameService + sudo systemctl enable $nameService 2>&1 if ($LASTEXITCODE -ne 0) { # Service registration failed. throw ($PodeLocale.serviceRegistrationException -f $nameService) @@ -399,8 +399,6 @@ WantedBy=multi-user.target return $true } - - <# .SYNOPSIS Registers a new Windows service to run a Pode-based PowerShell worker. diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 70f78b7a2..3c3f8e9fb 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -293,28 +293,36 @@ function Start-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } } elseif ($IsLinux) { + $nameService = "$Name.service".Replace(' ', '\x20') # Check if the service exists - if (systemctl status "$Name.service" -q) { + systemctl status $nameService 2>&1 + if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { # Check if the service is already running - $status = systemctl is-active "$Name.service" + $status = systemctl is-active $nameService if ($status -ne 'active') { - sudo systemctl start "$Name.service" - # Log service started successfully - # Write-PodeServiceLog -Message "Service '$Name' started successfully." + sudo systemctl start $nameService + $status = systemctl is-active $nameService + if ($status -ne 'active') { + throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $nameService) + } + else { + + # Write-PodeServiceLog -Message "Service '$nameService' started successfully."} + } } else { # Log service is already running - # Write-PodeServiceLog -Message "Service '$Name' is already running." + # Write-PodeServiceLog -Message "Service '$nameService' is already running." } } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) } } @@ -406,27 +414,35 @@ function Stop-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } } elseif ($IsLinux) { - # Check if the service exists - if (systemctl status "$Name.service" -q) { - $status = systemctl is-active "$Name.service" - if ($status -eq 'active') { - sudo systemctl stop "$Name.service" - # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + $nameService = "$Name.service".Replace(' ', '\x20') + systemctl status $nameService 2>&1 + # Check if the service is already registered + if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { + # Check if the service exists + if (systemctl status $nameService -q) { + $status = systemctl is-active $nameService + if ($status -eq 'active') { + sudo systemctl stop $nameService + $status = systemctl is-active $nameService + if ($status -eq 'active') { + throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) + } + else { + + # Write-PodeServiceLog -Message "Service '$Name' stopped successfully."} + } + } } else { - # Write-PodeServiceLog -Message "Service '$Name' is not running." + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") - } } - elseif ($IsMacOS) { # Check if the service exists in launchctl if (launchctl list | Select-String "pode.$Name") { @@ -456,6 +472,7 @@ function Stop-PodeService { return $true } + <# .SYNOPSIS Unregisters a Pode-based service across different platforms (Windows, Linux, and macOS). @@ -556,34 +573,42 @@ function Unregister-PodeService { elseif ($IsLinux) { try { - # Check if the service exists - if (systemctl status "$Name.service" -q) { + $nameService = "$Name.service".Replace(' ', '\x20') + systemctl status $nameService 2>&1 + # Check if the service is already registered + if ($code -eq 0 -or $code -eq 3) { # Check if the service is running - $status = systemctl is-active "$Name.service" + $status = systemctl is-active $nameService 2>&1 if ($status -eq 'active') { + # $status -eq 'active' if ($Force.IsPresent) { - sudo systemctl stop "$Name.service" + sudo systemctl stop $nameService # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f "$Name.service") + throw ($Podelocale.serviceIsRunningException -f $nameService) } } - sudo systemctl disable "$Name.service" - - # Read the content of the service file - $serviceFilePath = "/etc/systemd/system/$Name.service" - $serviceFileContent = Get-Content -Path $serviceFilePath + sudo systemctl disable $nameService + if ($LASTEXITCODE -eq 0 ) { + # Read the content of the service file + $serviceFilePath = "/etc/systemd/system/$nameService" + $serviceFileContent = Get-Content -Path $serviceFilePath + + # Extract the SettingsFile from the ExecStart line using regex + $settingsFile = $serviceFileContent | Select-String -Pattern 'ExecStart=.*\s+(.*)' | ForEach-Object { $_.Matches[0].Groups[1].Value } + if ((Test-Path -Path $settingsFile -PathType Leaf)) { + Remove-Item -Path $settingsFile + } - # Extract the SettingsFile from the ExecStart line using regex - $settingsFile = $serviceFileContent | Select-String -Pattern 'ExecStart=.*\s+(.*)' | ForEach-Object { $_.Matches[0].Groups[1].Value } - if ((Test-Path -Path $settingsFile -PathType Leaf)) { - Remove-Item -Path $settingsFile + Remove-Item -Path $serviceFilePath -ErrorAction Break + # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + } + else { + # Write-PodeServiceLog -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) } - - Remove-Item -Path $serviceFilePath -ErrorAction Break - # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } else { # Service is not registered From 36e14cc98c7459d0db7b227939ac482b2bbf70d6 Mon Sep 17 00:00:00 2001 From: Max Daneri Date: Mon, 21 Oct 2024 16:13:16 -0400 Subject: [PATCH 16/93] fix --- src/Public/Service.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 3c3f8e9fb..ba9a83e73 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -576,7 +576,7 @@ function Unregister-PodeService { $nameService = "$Name.service".Replace(' ', '\x20') systemctl status $nameService 2>&1 # Check if the service is already registered - if ($code -eq 0 -or $code -eq 3) { + if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { # Check if the service is running $status = systemctl is-active $nameService 2>&1 if ($status -eq 'active') { From 9e76fbb736fd51009fc7861e2eeb8f6313920292 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 21 Oct 2024 13:14:34 -0700 Subject: [PATCH 17/93] fix service path --- src/Private/Service.ps1 | 2 +- src/Public/Service.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index faab9c5e6..a66177f45 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -371,7 +371,7 @@ Group=$Group WantedBy=multi-user.target "@ | Set-Content -Path $tempFile -Encoding UTF8 - sudo cp $tempFile "/etc/systemd/system/$nameService" + sudo cp $tempFile "/etc/systemd/system/$Name.service" # Create user if needed if (!$SkipUserCreation.IsPresent) { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index ba9a83e73..ee9af1d56 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -547,7 +547,7 @@ function Unregister-PodeService { } # Remove the service - $null = Invoke-PodeWinElevatedCommand -Command 'Remove-Service' -Arguments "-Name '$Name'" + $null = Invoke-PodeWinElevatedCommand -Command 'Remove-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($null -ne $service) { # Write-PodeServiceLog -Message "Service '$Name' unregistered failed." @@ -593,7 +593,7 @@ function Unregister-PodeService { sudo systemctl disable $nameService if ($LASTEXITCODE -eq 0 ) { # Read the content of the service file - $serviceFilePath = "/etc/systemd/system/$nameService" + $serviceFilePath = "/etc/systemd/system/$Name.service" $serviceFileContent = Get-Content -Path $serviceFilePath # Extract the SettingsFile from the ExecStart line using regex From 5c0c33db5f5b532edaa57d35afcdb8b1f13b8165 Mon Sep 17 00:00:00 2001 From: Max Daneri Date: Mon, 21 Oct 2024 18:17:39 -0400 Subject: [PATCH 18/93] fixes --- examples/HelloService/HelloService.ps1 | 10 ++-- src/Private/Service.ps1 | 8 +-- src/Public/Service.ps1 | 78 ++++++++++++++++---------- 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 21608d970..a9338c4f6 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -103,25 +103,25 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service' + Register-PodeService -Name 'Hello Service2' -User 'm.daneri' exit } if ( $Unregister.IsPresent) { - Unregister-PodeService -Name 'Hello Service' -Force:$Force + Unregister-PodeService -Name 'Hello Service2' -Force:$Force exit } if ($Start.IsPresent) { - Start-PodeService -Name 'Hello Service' + Start-PodeService -Name 'Hello Service2' exit } if ($Stop.IsPresent) { - Stop-PodeService -Name 'Hello Service' + Stop-PodeService -Name 'Hello Service2' exit } if ($Query.IsPresent) { - Get-PodeService -Name 'Hello Service' + Get-PodeService -Name 'Hello Service2' exit } # Alternatively, you can directly import the Pode module from the system diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index a66177f45..7b09400ec 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -342,7 +342,7 @@ function Register-PodeLinuxService { [string] $OsArchitecture ) - $nameService = "$Name.service".Replace(' ', '\x20') + $nameService = "$Name.service".Replace(' ', '_') systemctl status $nameService 2>&1 # Check if the service is already registered @@ -359,11 +359,11 @@ Description=$Description After=network.target [Service] -ExecStart=$BinPath/$OsArchitecture/PodeMonitor $SettingsFile +ExecStart=$BinPath/$OsArchitecture/PodeMonitor "$SettingsFile" WorkingDirectory=$BinPath Restart=always User=$User -Group=$Group +#Group=$Group # Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1 # Environment=ASPNETCORE_ENVIRONMENT=Production @@ -371,7 +371,7 @@ Group=$Group WantedBy=multi-user.target "@ | Set-Content -Path $tempFile -Encoding UTF8 - sudo cp $tempFile "/etc/systemd/system/$Name.service" + sudo cp $tempFile "/etc/systemd/system/$nameService" # Create user if needed if (!$SkipUserCreation.IsPresent) { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index ee9af1d56..2832e75b8 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -298,7 +298,7 @@ function Start-PodeService { } elseif ($IsLinux) { - $nameService = "$Name.service".Replace(' ', '\x20') + $nameService = "$Name.service".Replace(' ', '_') # Check if the service exists systemctl status $nameService 2>&1 if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { @@ -418,7 +418,7 @@ function Stop-PodeService { } } elseif ($IsLinux) { - $nameService = "$Name.service".Replace(' ', '\x20') + $nameService = "$Name.service".Replace(' ', '_') systemctl status $nameService 2>&1 # Check if the service is already registered if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { @@ -573,7 +573,7 @@ function Unregister-PodeService { elseif ($IsLinux) { try { - $nameService = "$Name.service".Replace(' ', '\x20') + $nameService = "$Name.service".Replace(' ', '_') systemctl status $nameService 2>&1 # Check if the service is already registered if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { @@ -593,17 +593,24 @@ function Unregister-PodeService { sudo systemctl disable $nameService if ($LASTEXITCODE -eq 0 ) { # Read the content of the service file - $serviceFilePath = "/etc/systemd/system/$Name.service" - $serviceFileContent = Get-Content -Path $serviceFilePath + $serviceFilePath = "/etc/systemd/system/$nameService" + if ((Test-path -path $serviceFilePath -PathType Leaf)) { + $serviceFileContent = sudo cat $serviceFilePath + + # 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) + if ((Test-Path -Path $settingsFile -PathType Leaf)) { + Remove-Item -Path $settingsFile + } + sudo rm $serviceFilePath - # Extract the SettingsFile from the ExecStart line using regex - $settingsFile = $serviceFileContent | Select-String -Pattern 'ExecStart=.*\s+(.*)' | ForEach-Object { $_.Matches[0].Groups[1].Value } - if ((Test-Path -Path $settingsFile -PathType Leaf)) { - Remove-Item -Path $settingsFile + # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } - - Remove-Item -Path $serviceFilePath -ErrorAction Break - # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + sudo systemctl daemon-reload } else { # Write-PodeServiceLog -Message "Service '$Name' unregistered failed." @@ -768,35 +775,48 @@ function Get-PodeService { elseif ($IsLinux) { try { + $nameService = "$Name.service".Replace(' ', '_') # Check if the service exists on Linux (systemd) - $output = systemctl is-active "$Name.service" 2>&1 - if ($LASTEXITCODE -eq 0) { - if ($output -match 'active') { + $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}') $status = 'Running' } - elseif ($output -match 'inactive \(dead\)') { + '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 = 'Paused' + } + 'inactive' { + $status = 'Stopped' + } + 'failed' { $status = 'Stopped' } - elseif ($output -match 'activating') { + 'activating' { + $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') $status = 'Starting' } - elseif ($output -match 'deactivating') { + 'deactivating' { $status = 'Stopping' } - else { - $status = 'Unknown' - } - return @{ - Name = $Name - Status = $status + default { + $status = 'Stopped' } } - else { - return @{ - Name = $Name - Status = 'Stopped' - } + return @{ + Name = $Name + Status = $status + Pid = $servicePid } + + } catch { $_ | Write-PodeErrorLog From 1e0cf3b474338eb51a9933da1bcfefaf9b99a7ea Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 21 Oct 2024 18:46:03 -0700 Subject: [PATCH 19/93] Add logs --- .../Hello Service_srvsettings.json | 12 -- examples/HelloService/HelloService.ps1 | 5 +- .../HelloService2_srvsettings.json | 12 -- .../HelloService_srvsettings.json | 12 -- .../PetStore/Petstore-OpenApiMultiTag.ps1 | 2 - src/Pode.psd1 | 4 +- src/Private/Logging.ps1 | 30 +++- src/Private/Service.ps1 | 130 +++++++++++------- src/Public/Service.ps1 | 102 ++++++++++---- 9 files changed, 194 insertions(+), 115 deletions(-) delete mode 100644 examples/HelloService/Hello Service_srvsettings.json delete mode 100644 examples/HelloService/HelloService2_srvsettings.json delete mode 100644 examples/HelloService/HelloService_srvsettings.json diff --git a/examples/HelloService/Hello Service_srvsettings.json b/examples/HelloService/Hello Service_srvsettings.json deleted file mode 100644 index daa644657..000000000 --- a/examples/HelloService/Hello Service_srvsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "PodePwshWorker": { - "PwshPath": "/opt/microsoft/powershell/7/pwsh", - "LogFilePath": "/home/m.daneri/Documents/Pode/examples/HelloService/logs/Hello Service_svc.log", - "ParameterString": "", - "ScriptPath": "/home/m.daneri/Documents/Pode/examples/HelloService/HelloService.ps1", - "DisableTermination": true, - "Quiet": true, - "Name": "Hello Service", - "ShutdownWaitTimeMs": 30000 - } -} diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index a9338c4f6..0eeb932ee 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -103,7 +103,7 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service2' -User 'm.daneri' + Register-PodeService -Name 'Hello Service2' exit } if ( $Unregister.IsPresent) { @@ -129,6 +129,9 @@ if ($Query.IsPresent) { # Start the Pode server Start-PodeServer { + New-PodeLoggingMethod -File -Name 'service' -MaxDays 4 | Enable-PodeServiceLogging + New-PodeLoggingMethod -File -Name 'errors' -MaxDays 4 | Enable-PodeErrorLogging + # Add an HTTP endpoint listening on localhost at port 8080 Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http diff --git a/examples/HelloService/HelloService2_srvsettings.json b/examples/HelloService/HelloService2_srvsettings.json deleted file mode 100644 index 67d1d7f98..000000000 --- a/examples/HelloService/HelloService2_srvsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "PodePwshWorker": { - "DisableTermination": true, - "Quiet": true, - "Name": "HelloService2", - "ParameterString": "", - "ShutdownWaitTimeMs": 30000, - "LogFilePath": "/home/m.daneri/Documents/Pode/examples/HelloService/logs/HelloService2_svc.log", - "PwshPath": "/opt/microsoft/powershell/7/pwsh", - "ScriptPath": "/home/m.daneri/Documents/Pode/examples/HelloService/HelloService.ps1" - } -} diff --git a/examples/HelloService/HelloService_srvsettings.json b/examples/HelloService/HelloService_srvsettings.json deleted file mode 100644 index 647d9f32b..000000000 --- a/examples/HelloService/HelloService_srvsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "PodePwshWorker": { - "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", - "Quiet": true, - "DisableTermination": true, - "ParameterString": "", - "Name": "HelloService", - "ScriptPath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", - "ShutdownWaitTimeMs": 30000, - "LogFilePath": "C:\\Users\\mdaneri\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\HelloService_svc.log" - } -} diff --git a/examples/PetStore/Petstore-OpenApiMultiTag.ps1 b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 index a818977da..7987cee27 100644 --- a/examples/PetStore/Petstore-OpenApiMultiTag.ps1 +++ b/examples/PetStore/Petstore-OpenApiMultiTag.ps1 @@ -201,8 +201,6 @@ Some useful links: New-PodeAuthScheme -Basic -Realm 'PetStore' | Add-PodeAuth -Name 'Basic' -Sessionless -ScriptBlock { param($username, $password) - write-host $username - write-host $password # here you'd check a real user storage, this is just for example if ($username -eq 'morty' -and $password -eq 'pickle') { diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 1caa1fdae..0e96066e4 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -505,7 +505,9 @@ 'Unregister-PodeService', 'Start-PodeService', 'Stop-PodeService', - 'Get-PodeService' + 'Get-PodeService', + 'Enable-PodeServiceLogging', + 'Disable-PodeServiceLogging' ) # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index 27db3e4fa..7962e1e0b 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/Service.ps1 b/src/Private/Service.ps1 index 7b09400ec..3374bf19c 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -233,6 +233,8 @@ function Register-PodeMacService { "@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 + Write-PodeServiceLog -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + chmod +r "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" try { @@ -371,8 +373,12 @@ User=$User WantedBy=multi-user.target "@ | Set-Content -Path $tempFile -Encoding UTF8 + Write-PodeServiceLog -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + sudo cp $tempFile "/etc/systemd/system/$nameService" + Remove-Item -path $tempFile -ErrorAction SilentlyContinue + # Create user if needed if (!$SkipUserCreation.IsPresent) { # Run the id command to check if the user exists @@ -511,6 +517,7 @@ function Register-PodeWindowsService { if ($SecurityDescriptorSddl) { $params['SecurityDescriptorSddl'] = $SecurityDescriptorSddl } + Write-PodeServiceLog -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." try { $paramsString = $params.GetEnumerator() | ForEach-Object { "-$($_.Key) '$($_.Value)'" } @@ -563,72 +570,87 @@ function Write-PodeServiceLog { ) Process { - $Service = $PodeContext.Server.Service - if ($null -eq $Service ) { - $Service = @{Name = 'Not a service' } - } - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - - 'message' { - $logItem = @{ - Name = $Service.Name - Date = (Get-Date).ToUniversalTime() - Item = @{ - Level = $Level - Message = $Message - Tag = $Tag - } - } - break + if ($PodeContext -and $PodeContext.LogsToProcess) { + $Service = $PodeContext.Server.Service + if ($null -eq $Service ) { + $Service = @{Name = 'Not a service' } } - 'custom' { - $logItem = @{ - Name = $Service.Name - Date = (Get-Date).ToUniversalTime() - Item = @{ - Level = $Level - Message = $Message - Tag = $Tag + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + + 'message' { + $logItem = @{ + Name = $Service.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Level = $Level + Message = $Message + Tag = $Tag + } } + break } - break - } - 'exception' { - $logItem = @{ - Name = $Service.Name - Date = (Get-Date).ToUniversalTime() - Item = @{ - Category = $Exception.Source - Message = $Exception.Message - StackTrace = $Exception.StackTrace - Level = $Level + + 'exception' { + $logItem = @{ + Name = $Service.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Category = $Exception.Source + Message = $Exception.Message + StackTrace = $Exception.StackTrace + Level = $Level + } } + Write-PodeErrorLog -Level $Level -CheckInnerException:$CheckInnerException -Exception $Exception + break } - Write-PodeErrorLog -Level $Level -CheckInnerException:$CheckInnerException -Exception $Exception - } - 'error' { - $logItem = @{ - Name = $Service.Name - Date = (Get-Date).ToUniversalTime() - Item = @{ - Category = $ErrorRecord.CategoryInfo.ToString() - Message = $ErrorRecord.Exception.Message - StackTrace = $ErrorRecord.ScriptStackTrace - Level = $Level + 'error' { + $logItem = @{ + Name = $Service.Name + Date = (Get-Date).ToUniversalTime() + Item = @{ + Category = $ErrorRecord.CategoryInfo.ToString() + Message = $ErrorRecord.Exception.Message + StackTrace = $ErrorRecord.ScriptStackTrace + Level = $Level + } } + Write-PodeErrorLog -Level $Level -ErrorRecord $ErrorRecord + break } - Write-PodeErrorLog -Level $Level -ErrorRecord $ErrorRecord } + $null = $PodeContext.LogsToProcess.Add($logItem) } - $lpath = Get-PodeRelativePath -Path './logs' -JoinRoot - $logItem | ConvertTo-Json -Compress -Depth 5 | Add-Content "$lpath/watchdog-$($Service.Name).log" + else { + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + + 'message' { + Write-Verbose -Message $Message + break + } + + 'exception' { + Write-Error -Exception $Exception + break + } + + 'error' { + Write-Error "$($ErrorRecord.CategoryInfo.ToString()): $($ErrorRecord.Exception.Message)" + break + } + } + } + # $lpath = Get-PodeRelativePath -Path './logs' -JoinRoot + # $logItem | ConvertTo-Json -Compress -Depth 5 | Add-Content "$lpath/watchdog-$($Service.Name).log" } } + + function Test-PodeUserServiceCreationPrivilege { # Get the list of user privileges $privileges = whoami /priv | Where-Object { $_ -match 'SeCreateServicePrivilege' } @@ -676,4 +698,10 @@ function Confirm-PodeAdminPrivilege { Write-PodeHost 'Insufficient privileges. This script must be run as root or with sudo permissions to continue.' -ForegroundColor Red exit } -} \ No newline at end of file +} + + + +function Get-PodeServiceLoggingName { + return '__pode_log_service__' +} diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 2832e75b8..9dd00a62a 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -152,7 +152,8 @@ function Register-PodeService { $settingsPath = $MainScriptPath } $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_srvsettings.json" - + Write-PodeServiceLog -Message "Service '$Name' setting : $settingsFile." + # Generate the service settings JSON file $jsonContent = @{ PodePwshWorker = @{ @@ -280,7 +281,7 @@ function Start-PodeService { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Running') { - # Write-PodeServiceLog -Message "Service '$Name' started successfully." + Write-PodeServiceLog -Message "Service '$Name' started successfully." } else { throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $Name) @@ -288,7 +289,7 @@ function Start-PodeService { } else { # Log service is already running - # Write-PodeServiceLog -Message "Service '$Name' is already running." + Write-PodeServiceLog -Message "Service '$Name' is already running." } } else { @@ -312,12 +313,12 @@ function Start-PodeService { } else { - # Write-PodeServiceLog -Message "Service '$nameService' started successfully."} + Write-PodeServiceLog -Message "Service '$nameService' started successfully." } } else { # Log service is already running - # Write-PodeServiceLog -Message "Service '$nameService' is already running." + Write-PodeServiceLog -Message "Service '$nameService' is already running." } } else { @@ -337,12 +338,12 @@ function Start-PodeService { launchctl start "pode.$Name" # Log service started successfully - # Write-PodeServiceLog -Message "Service '$Name' started successfully." + Write-PodeServiceLog -Message "Service '$Name' started successfully." return ($LASTEXITCODE -eq 0) } else { # Log service is already running - # Write-PodeServiceLog -Message "Service '$Name' is already running." + Write-PodeServiceLog -Message "Service '$Name' is already running." } } else { @@ -402,14 +403,14 @@ function Stop-PodeService { $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Stopped') { - # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + Write-PodeServiceLog -Message "Service '$Name' stopped successfully." } else { throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } } else { - # Write-PodeServiceLog -Message "Service '$Name' is not running." + Write-PodeServiceLog -Message "Service '$Name' is not running." } } else { @@ -432,8 +433,7 @@ function Stop-PodeService { throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } else { - - # Write-PodeServiceLog -Message "Service '$Name' stopped successfully."} + Write-PodeServiceLog -Message "Service '$Name' stopped successfully." } } } @@ -452,11 +452,11 @@ function Stop-PodeService { # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { launchctl stop "pode.$Name" - # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + Write-PodeServiceLog -Message "Service '$Name' stopped successfully." return ($LASTEXITCODE -eq 0) } else { - # Write-PodeServiceLog -Message "Service '$Name' is not running." + Write-PodeServiceLog -Message "Service '$Name' is not running." } } else { @@ -534,7 +534,7 @@ function Unregister-PodeService { $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Stopped') { - # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) @@ -550,10 +550,10 @@ function Unregister-PodeService { $null = Invoke-PodeWinElevatedCommand -Command 'Remove-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($null -ne $service) { - # Write-PodeServiceLog -Message "Service '$Name' unregistered failed." + Write-PodeServiceLog -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } - # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." # Remove the service configuration if ($pathName) { @@ -583,7 +583,7 @@ function Unregister-PodeService { # $status -eq 'active' if ($Force.IsPresent) { sudo systemctl stop $nameService - # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { # Service is running. Use the -Force parameter to forcefully stop." @@ -608,12 +608,12 @@ function Unregister-PodeService { } sudo rm $serviceFilePath - # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } sudo systemctl daemon-reload } else { - # Write-PodeServiceLog -Message "Service '$Name' unregistered failed." + Write-PodeServiceLog -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } } @@ -638,19 +638,19 @@ function Unregister-PodeService { # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { launchctl stop "pode.$Name" - # Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + Write-PodeServiceLog -Message "Service '$Name' stopped successfully." $serviceIsRunning = ($LASTEXITCODE -ne 0) } else { $serviceIsRunning = $false - # Write-PodeServiceLog -Message "Service '$Name' is not running." + Write-PodeServiceLog -Message "Service '$Name' is not running." } # Check if the service is running if ( $serviceIsRunning) { if ($Force.IsPresent) { launchctl stop "pode.$Name" - # Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." } else { # Service is running. Use the -Force parameter to forcefully stop." @@ -679,7 +679,7 @@ function Unregister-PodeService { else { return $false } - # Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." } else { # Service is not registered @@ -860,3 +860,59 @@ function Get-PodeService { } } } + + + +function Enable-PodeServiceLogging { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable] + $Method, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug', '*')] + [string[]] + $Levels = @('Error'), + + [switch] + $Raw + ) + + $name = Get-PodeServiceLoggingName + + # error if it's already enabled + if ($PodeContext.Server.Logging.Types.Contains($name)) { + # Error Logging has already been enabled + throw ($PodeLocale.errorLoggingAlreadyEnabledExceptionMessage) + } + + # ensure the Method contains a scriptblock + if (Test-PodeIsEmpty $Method.ScriptBlock) { + # The supplied output Method for Error Logging requires a valid ScriptBlock + throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Error') + } + + # all errors? + if ($Levels -contains '*') { + $Levels = @('Error', 'Warning', 'Informational', 'Verbose', 'Debug') + } + + # add the error logger + $PodeContext.Server.Logging.Types[$name] = @{ + Method = $Method + ScriptBlock = (Get-PodeLoggingInbuiltType -Type Errors) + Arguments = @{ + Raw = $Raw + Levels = $Levels + } + } +} + +function Disable-PodeServiceLogging { + [CmdletBinding()] + param() + + Remove-PodeLogger -Name (Get-PodeServiceLoggingName) +} From b19643d7dfa1f52807c7420d0d69525d5ac8071f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 21 Oct 2024 18:56:28 -0700 Subject: [PATCH 20/93] Update Service.ps1 --- src/Public/Service.ps1 | 45 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 9dd00a62a..0860a6c40 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -153,7 +153,7 @@ function Register-PodeService { } $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_srvsettings.json" Write-PodeServiceLog -Message "Service '$Name' setting : $settingsFile." - + # Generate the service settings JSON file $jsonContent = @{ PodePwshWorker = @{ @@ -862,7 +862,35 @@ function Get-PodeService { } +<# +.SYNOPSIS +Enables logging for the Pode service using a specified logging method. + +.DESCRIPTION +The `Enable-PodeServiceLogging` function configures and enables service logging for the Pode server using the provided logging method and specified log levels. It ensures that the logging method includes a valid script block and prevents duplicate logging methods from being enabled. + +.PARAMETER Method +A hashtable that defines the logging method. This should contain a `ScriptBlock` key, which specifies the script to be executed for logging. + +.PARAMETER Levels +An array of logging levels to capture. The available levels are 'Error', 'Warning', 'Informational', 'Verbose', 'Debug', or '*'. The default value is 'Error'. If '*' is specified, all levels are captured. + +.PARAMETER Raw +Indicates whether to log raw data without formatting. If set, the output is logged as-is without additional processing. + +.EXAMPLE +PS> Enable-PodeServiceLogging -Method @{ ScriptBlock = { Write-Host "Logging" } } -Levels 'Error', 'Warning' + +Enables error and warning level logging using the provided method. + +.EXAMPLE +PS> Enable-PodeServiceLogging -Method @{ ScriptBlock = { Write-Host "Raw Logging" } } -Raw + +Enables raw logging for all error levels. +.NOTES +This function throws an error if the logging method has already been enabled or if the provided method does not include a valid script block. +#> function Enable-PodeServiceLogging { [CmdletBinding()] param( @@ -910,6 +938,21 @@ function Enable-PodeServiceLogging { } } +<# +.SYNOPSIS +Disables the logging for the Pode service. + +.DESCRIPTION +The `Disable-PodeServiceLogging` function disables the currently enabled logging method for the Pode service. It removes the logger associated with the service by using the logger's name. + +.EXAMPLE +PS> Disable-PodeServiceLogging + +Disables the service logging for Pode. + +.NOTES +This function uses the `Remove-PodeLogger` cmdlet to remove the logger by name. +#> function Disable-PodeServiceLogging { [CmdletBinding()] param() From 8a314ea045bfc852944a604d298428aa65d6e662 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 22 Oct 2024 09:50:20 -0700 Subject: [PATCH 21/93] Code completed --- docs/Hosting/PortsBelow1024.md | 55 ++++ docs/Hosting/RunAsService.md | 121 +++++++- examples/HelloService/HelloService.ps1 | 5 +- .../Hello Service2_svcsettings.json | 12 + src/Pode.psd1 | 4 +- src/PodePwshMonitor/PodePwshMonitor.cs | 47 ++- src/Private/Service.ps1 | 215 ++++---------- src/Public/Service.ps1 | 281 ++++++------------ src/Public/Utilities.ps1 | 29 +- 9 files changed, 372 insertions(+), 397 deletions(-) create mode 100644 docs/Hosting/PortsBelow1024.md create mode 100644 examples/HelloService/svc_settings/Hello Service2_svcsettings.json 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 f880dc63c..d06541826 100644 --- a/docs/Hosting/RunAsService.md +++ b/docs/Hosting/RunAsService.md @@ -1,8 +1,121 @@ -# 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 now provides built-in functions to easily manage services across platforms (Windows, Linux, macOS). These functions allow you to register, start, stop, query, and unregister Pode services in a cross-platform way. -## Windows +## Registering a Service + +You can register a Pode-based service using the `Register-PodeService` function, which will create the necessary service files and configurations for your system. + +#### Example: +```powershell +Register-PodeService -Name "HelloService" -Description "Example Pode Service" -ParameterString "-Verbose" -Start +``` + +This command registers a service named "HelloService" and starts it immediately after registration. The service runs your Pode script with the specified parameters. + +### `Register-PodeService` Parameters + +The `Register-PodeService` function provides several parameters to customize your service registration across Windows, Linux, and macOS: + +- **`-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 milliseconds. + +- **`-UserName`** *(string)*: + Specifies the username under which the service will run by default is the current user. + +- **`-Start`** *(switch)*: + A switch to start 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)*: + Specifies the directory to store the service configuration file (`_svcsettings.json`). If not provided, a default directory is used. + +- **`-LogPath`** *(string)*: + Specifies the path for the service log files. If not provided, a default log directory is used. + +--- + +## Starting a Service + +Once a service is registered, you can start it using the `Start-PodeService` function. + +#### Example: +```powershell +Start-PodeService -Name "HelloService" +``` + +This returns $true if the service is started successfully, $false otherwise. + +## Stopping a Service + +To stop a running Pode service, you can use the `Stop-PodeService` function. + +#### Example: +```powershell +Stop-PodeService -Name "HelloService" +``` +This returns $true if it was stopped, $false otherwise. + +## Querying a Service + +To check the status of a service (whether it's running or stopped), use the `Get-PodeService` function. + +#### Example: +```powershell +Get-PodeService -Name "HelloService" +``` + +This returns a hashtable with the service name and status. +```powershell +Name Value +---- ----- +Status Running +Pid 17576 +Name HelloService +``` + +## Unregistering a Service + +When you're done with a service, you can unregister it using the `Unregister-PodeService` function. You can also forcefully stop and remove a service using the `-Force` parameter. + +#### Example: +```powershell +Unregister-PodeService -Name "HelloService" -Force +``` + +This returns $true if it was unregistered successfully, $false otherwise. + + + +# Alternative Methods for Windows and Linux + +If you prefer to manually manage Pode as a service or if you're working in an environment where the Pode functions are unavailable, you can still use the traditional methods for managing services on Windows and Linux. + +#### Windows (NSSM): 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: @@ -45,7 +158,7 @@ nssm stop $name nssm remove $name confirm ``` -## Linux +#### Linux (systemd): 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: diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 0eeb932ee..14d3f09c9 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -124,13 +124,10 @@ if ($Query.IsPresent) { Get-PodeService -Name 'Hello Service2' exit } -# Alternatively, you can directly import the Pode module from the system -# Import-Module Pode # Start the Pode server Start-PodeServer { - New-PodeLoggingMethod -File -Name 'service' -MaxDays 4 | Enable-PodeServiceLogging - New-PodeLoggingMethod -File -Name 'errors' -MaxDays 4 | Enable-PodeErrorLogging + New-PodeLoggingMethod -File -Name 'errors' -MaxDays 4 -Path './logs' | Enable-PodeErrorLogging # Add an HTTP endpoint listening on localhost at port 8080 Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http diff --git a/examples/HelloService/svc_settings/Hello Service2_svcsettings.json b/examples/HelloService/svc_settings/Hello Service2_svcsettings.json new file mode 100644 index 000000000..9f48d3e53 --- /dev/null +++ b/examples/HelloService/svc_settings/Hello Service2_svcsettings.json @@ -0,0 +1,12 @@ +{ + "PodePwshWorker": { + "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\Hello Service2_svc.log", + "ParameterString": "", + "Name": "Hello Service2", + "ShutdownWaitTimeMs": 30000, + "ScriptPath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", + "Quiet": true, + "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", + "DisableTermination": true + } +} diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 0e96066e4..1caa1fdae 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -505,9 +505,7 @@ 'Unregister-PodeService', 'Start-PodeService', 'Stop-PodeService', - 'Get-PodeService', - 'Enable-PodeServiceLogging', - 'Disable-PodeServiceLogging' + 'Get-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/PodePwshMonitor/PodePwshMonitor.cs b/src/PodePwshMonitor/PodePwshMonitor.cs index 006f54d0a..55d925a43 100644 --- a/src/PodePwshMonitor/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/PodePwshMonitor.cs @@ -96,7 +96,7 @@ public void StartPowerShellProcess() // Build the PowerShell command with NoProfile and global variable initialization string command = $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; - Log($"Starting PowerShell process with command: {command}"); + Log($"[Server] - Starting PowerShell process with command: {command}"); // Set the arguments for the PowerShell process _powerShellProcess.StartInfo.Arguments = command; @@ -104,15 +104,6 @@ public void StartPowerShellProcess() // Start the process _powerShellProcess.Start(); - // Enable raising events to capture process exit and handle cleanup - /* _powerShellProcess.EnableRaisingEvents = true; - _powerShellProcess.Exited += (sender, args) => - { - Log("PowerShell process exited."); - _powerShellProcess.Dispose(); - _powerShellProcess = null; - };*/ - // Log output and error asynchronously _powerShellProcess.OutputDataReceived += (sender, args) => Log(args.Data); _powerShellProcess.ErrorDataReceived += (sender, args) => Log(args.Data); @@ -120,11 +111,11 @@ public void StartPowerShellProcess() _powerShellProcess.BeginErrorReadLine(); _lastLogTime = DateTime.Now; - Log("PowerShell process started successfully."); + Log("[Server] - PowerShell process started successfully."); } catch (Exception ex) { - Log($"Failed to start PowerShell process: {ex.Message}"); + Log($"[Server] - Failed to start PowerShell process: {ex.Message}"); } } else @@ -132,7 +123,7 @@ public void StartPowerShellProcess() // Log only if more than a minute has passed since the last log if ((DateTime.Now - _lastLogTime).TotalMinutes >= 1) { - Log("PowerShell process is already running."); + Log("[Server] - PowerShell process is already running."); _lastLogTime = DateTime.Now; } } @@ -143,7 +134,7 @@ public void StopPowerShellProcess() try { _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Log($"Connecting to the pipe server using pipe: {_pipeName}"); + Log($"[Server] - Connecting to the pipe server using pipe: {_pipeName}"); // Connect to the PowerShell pipe server _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection @@ -152,7 +143,7 @@ public void StopPowerShellProcess() { // Send shutdown message and wait for the process to exit SendPipeMessage("shutdown"); - Log($"Waiting up to {_shutdownWaitTimeMs} milliseconds for the PowerShell process to exit..."); + Log($"[Server] - Waiting up to {_shutdownWaitTimeMs} milliseconds for the PowerShell process to exit..."); // Timeout logic int waited = 0; @@ -166,16 +157,16 @@ public void StopPowerShellProcess() if (_powerShellProcess.HasExited) { - Log("PowerShell process has been shutdown gracefully."); + Log("[Server] - PowerShell process has been shutdown gracefully."); } else { - Log($"PowerShell process did not exit in {_shutdownWaitTimeMs} milliseconds."); + Log($"[Server] - PowerShell process did not exit in {_shutdownWaitTimeMs} milliseconds."); } } else { - Log($"Failed to connect to the PowerShell pipe server using pipe: {_pipeName}"); + Log($"[Server] - Failed to connect to the PowerShell pipe server using pipe: {_pipeName}"); } // Forcefully kill the process if it's still running @@ -184,17 +175,17 @@ public void StopPowerShellProcess() try { _powerShellProcess.Kill(); - Log("PowerShell process killed successfully."); + Log("[Server] - PowerShell process killed successfully."); } catch (Exception ex) { - Log($"Error killing PowerShell process: {ex.Message}"); + Log($"[Server] - Error killing PowerShell process: {ex.Message}"); } } } catch (Exception ex) { - Log($"Error stopping PowerShell process: {ex.Message}"); + Log($"[Server] - Error stopping PowerShell process: {ex.Message}"); } finally { @@ -210,7 +201,7 @@ public void StopPowerShellProcess() _pipeClient?.Dispose(); _pipeClient = null; } - Log("PowerShell process and pipe client disposed."); + Log("[Server] - PowerShell process and pipe client disposed."); } } @@ -220,7 +211,7 @@ public void RestartPowerShellProcess() if (_pipeClient != null && _pipeClient.IsConnected) { SendPipeMessage("restart"); // Inform PowerShell about the restart - Log("Restart message sent to PowerShell."); + Log("[Server] - Restart message sent to PowerShell."); } } @@ -228,13 +219,13 @@ private void SendPipeMessage(string message) { if (_pipeClient == null) { - Log("Pipe client is not initialized, cannot send message."); + Log("[Server] - Pipe client is not initialized, cannot send message."); return; } if (!_pipeClient.IsConnected) { - Log("Pipe client is not connected, cannot send message."); + Log("[Server] - Pipe client is not connected, cannot send message."); return; } @@ -245,12 +236,12 @@ private void SendPipeMessage(string message) { writer.AutoFlush = true; writer.WriteLine(message); - Log($"Message sent to PowerShell: {message}"); + Log($"[Server] - Message sent to PowerShell: {message}"); } } catch (Exception ex) { - Log($"Failed to send message to PowerShell: {ex.Message}"); + Log($"[Server] - Failed to send message to PowerShell: {ex.Message}"); } } @@ -268,7 +259,7 @@ private void Log(string data) } catch (Exception ex) { - Console.WriteLine($"Failed to log to file: {ex.Message}"); + Console.WriteLine($"[Server] - Failed to log to file: {ex.Message}"); } } } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 3374bf19c..322c6cfb7 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -51,7 +51,6 @@ function Test-PodeServiceEnabled { Global variable example: $global:PodeService=@{DisableTermination=$true;Quiet=$false;Pipename='ssss'} #> - function Start-PodeServiceHearthbeat { # Check if the Pode service is enabled @@ -59,60 +58,67 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { - Write-PodeServiceLog -Message "Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" - try { - # Create a named pipe server stream - $pipeStream = [System.IO.Pipes.NamedPipeServerStream]::new( - $PodeContext.Server.Service.PipeName, - [System.IO.Pipes.PipeDirection]::InOut, - 2, # Max number of allowed concurrent connections - [System.IO.Pipes.PipeTransmissionMode]::Byte, - [System.IO.Pipes.PipeOptions]::None - ) - - Write-PodeServiceLog -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." - $pipeStream.WaitForConnection() # Wait until a client connects - Write-PodeServiceLog -Message "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 - while ($pipeStream.IsConnected) { - $message = $reader.ReadLine() # Read message from the pipe - - if ($message) { - Write-PodeServiceLog -Message "Received message: $message" - - # Process 'shutdown' message - if ($message -eq 'shutdown') { - Write-PodeServiceLog -Message 'Server requested shutdown. Closing client...' - Close-PodeServer # Gracefully stop the Pode server - break # Exit the loop - - # Process 'restart' message + Write-PodeHost -Message "[Client] - Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" -Force + + while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + 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 + ) + + Write-PodeHost -Message "[Client] - Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." -Force + $pipeStream.WaitForConnection() # Wait until a client connects + Write-PodeHost -Message "[Client] - Connected to the $($PodeContext.Server.Service.PipeName) pipe." -Force + + # 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 + while ($pipeStream.IsConnected) { + $message = $reader.ReadLine() # Read message from the pipe + if ( $PodeContext.Tokens.Cancellation.IsCancellationRequested) { + return } - elseif ($message -eq 'restart') { - Write-PodeServiceLog -Message 'Server requested restart. Restarting client...' - Restart-PodeServer # Restart the Pode server - break # Exit the loop + if ($message) { + Write-PodeHost -Message "[Client] - Received message: $message" -Force + + # Process 'shutdown' message + if ($message -eq 'shutdown') { + + Write-PodeHost -Message '[Client] - Server requested shutdown. Closing client...' -Force + Close-PodeServer # Gracefully stop the Pode server + return # Exit the loop + + # Process 'restart' message + } + elseif ($message -eq 'restart') { + Write-PodeHost -Message '[Client] - Server requested restart. Restarting client...' -Force + Restart-PodeServer # Restart the Pode server + return # Exit the loop + } } } } + catch { + $_ | Write-PodeErrorLog # Log any errors that occur during pipe operation + throw $_ + } + finally { + $reader.Dispose() + $pipeStream.Dispose() # Always dispose of the pipe stream when done + } } - catch { - $_ | Write-PodeServiceLog # Log any errors that occur during pipe operation - } - finally { - $reader.Dispose() - $pipeStream.Dispose() # Always dispose of the pipe stream when done - } - } # Assign a name to the Pode service $PodeContext.Server.Service['Name'] = 'Service' - Write-PodeServiceLog -Message 'Starting service monitoring' + 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 @@ -233,7 +239,7 @@ function Register-PodeMacService { "@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 - Write-PodeServiceLog -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + Write-Verbose -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." chmod +r "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" @@ -373,7 +379,7 @@ User=$User WantedBy=multi-user.target "@ | Set-Content -Path $tempFile -Encoding UTF8 - Write-PodeServiceLog -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + Write-Verbose -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." sudo cp $tempFile "/etc/systemd/system/$nameService" @@ -517,7 +523,7 @@ function Register-PodeWindowsService { if ($SecurityDescriptorSddl) { $params['SecurityDescriptorSddl'] = $SecurityDescriptorSddl } - Write-PodeServiceLog -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + Write-Verbose -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." try { $paramsString = $params.GetEnumerator() | ForEach-Object { "-$($_.Key) '$($_.Value)'" } @@ -537,117 +543,6 @@ function Register-PodeWindowsService { } -function Write-PodeServiceLog { - [CmdletBinding(DefaultParameterSetName = 'Message')] - param( - - - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Exception')] - [System.Exception] - $Exception, - - [Parameter(ParameterSetName = 'Exception')] - [switch] - $CheckInnerException, - - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Error')] - [System.Management.Automation.ErrorRecord] - $ErrorRecord, - - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Message')] - [string] - $Message, - - [string] - $Level = 'Informational', - - [string] - $Tag = '-', - - [Parameter()] - [int] - $ThreadId - - ) - Process { - if ($PodeContext -and $PodeContext.LogsToProcess) { - $Service = $PodeContext.Server.Service - if ($null -eq $Service ) { - $Service = @{Name = 'Not a service' } - } - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - - 'message' { - $logItem = @{ - Name = $Service.Name - Date = (Get-Date).ToUniversalTime() - Item = @{ - Level = $Level - Message = $Message - Tag = $Tag - } - } - break - } - - 'exception' { - $logItem = @{ - Name = $Service.Name - Date = (Get-Date).ToUniversalTime() - Item = @{ - Category = $Exception.Source - Message = $Exception.Message - StackTrace = $Exception.StackTrace - Level = $Level - } - } - Write-PodeErrorLog -Level $Level -CheckInnerException:$CheckInnerException -Exception $Exception - break - } - - 'error' { - $logItem = @{ - Name = $Service.Name - Date = (Get-Date).ToUniversalTime() - Item = @{ - Category = $ErrorRecord.CategoryInfo.ToString() - Message = $ErrorRecord.Exception.Message - StackTrace = $ErrorRecord.ScriptStackTrace - Level = $Level - } - } - Write-PodeErrorLog -Level $Level -ErrorRecord $ErrorRecord - break - } - } - $null = $PodeContext.LogsToProcess.Add($logItem) - } - - else { - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - - 'message' { - Write-Verbose -Message $Message - break - } - - 'exception' { - Write-Error -Exception $Exception - break - } - - 'error' { - Write-Error "$($ErrorRecord.CategoryInfo.ToString()): $($ErrorRecord.Exception.Message)" - break - } - } - } - # $lpath = Get-PodeRelativePath -Path './logs' -JoinRoot - # $logItem | ConvertTo-Json -Compress -Depth 5 | Add-Content "$lpath/watchdog-$($Service.Name).log" - - } -} - diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 0860a6c40..681c22f21 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -1,12 +1,11 @@ <# .SYNOPSIS - Registers a new service to run a Pode-based PowerShell worker as a service on Windows, Linux, or macOS. + 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 dynamically creates a service with the specified parameters, including - paths to the worker script, log files, and service-specific settings. The function also generates a `srvsettings.json` file, - containing the service configuration. The service can optionally be started immediately after registration, based on the platform. + 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. @@ -15,57 +14,51 @@ A brief description of the service. Defaults to "This is a Pode service." .PARAMETER DisplayName - Specifies the display name for the service in the Windows Services Manager. Defaults to "Pode Service($Name)". + Specifies the display name for the service (Windows only). Defaults to "Pode Service($Name)". .PARAMETER StartupType - Specifies the startup type of the service (e.g., 'Automatic', 'Manual'). Defaults to 'Automatic'. - -.PARAMETER SecurityDescriptorSddl - A security descriptor in SDDL format, specifying the permissions for the service (Windows only). + 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 run by the service. Defaults to an empty string. + Any additional parameters to pass to the worker script when the service is run. Defaults to an empty string. -.PARAMETER Quiet - If set to `$true`, runs the service quietly, suppressing logs and output. Defaults to `$true`. - -.PARAMETER DisableTermination - If set to `$true`, disables termination of the service from within the worker process. Defaults to `$true`. +.PARAMETER LogServicePodeHost + Enables logging for the Pode service host. .PARAMETER ShutdownWaitTimeMs - The maximum amount of time, in milliseconds, to wait for the service to shut down gracefully before forcefully terminating it. - Defaults to 30,000 milliseconds (30 seconds). - -.PARAMETER User - Specifies the user under which the service will run (applies to Linux and macOS). Defaults to `podeuser`. + Maximum time in milliseconds to wait for the service to shut down gracefully before forcing termination. Defaults to 30,000 milliseconds. -.PARAMETER Group - Specifies the group under which the service will run (Linux only). Defaults to `podeuser`. +.PARAMETER UserName + Specifies the username under which the service will run by default is the current user. .PARAMETER Start - A switch to immediately start the service after registration. + A switch to start the service immediately after registration. -.PARAMETER SkipUserCreation - A switch to skip the process of creating a new user (Linux only). +.PARAMETER Password + A secure password for the service account (Windows only). If omitted, the service account will be 'NT AUTHORITY\SYSTEM'. -.PARAMETER Credential - A `PSCredential` object specifying the credentials for the Windows service account under which the service will run. +.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 ConfigDirectory - Specifies a custom directory to store the generated configuration (`srvsettings.json`) file. +.PARAMETER LogPath + Specifies the path for the service log files. If not provided, a default log directory is used. .EXAMPLE Register-PodeService -Name "PodeExampleService" -Description "Example Pode Service" -ParameterString "-Verbose" - This example registers a new Pode service called "PodeExampleService" with verbose logging enabled. + This example registers a Pode service named "PodeExampleService" with verbose logging enabled. .NOTES - - The function supports cross-platform service registration on Windows, Linux, and macOS. - - A configuration file (`srvsettings.json`) is generated in the specified directory, or by default, in the same directory as the main script. - - On Windows, the function checks for appropriate permissions (e.g., Administrator or service creation privileges). - - The Pode service can be started automatically after registration using the `-Start` switch. - - The PowerShell executable path is dynamically obtained to ensure compatibility across environments. + - 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)] @@ -88,32 +81,26 @@ function Register-PodeService { [string] $ParameterString = '', - [bool] - $Quiet = $true, - - [bool] - $DisableTermination = $true, + [switch] + $LogServicePodeHost, [int] $ShutdownWaitTimeMs = 30000, [string] - $User = 'podeuser', - - [string] - $Group = 'podeuser', + $UserName, [switch] $Start, - [switch] - $SkipUserCreation, + [securestring] + $Password, - [pscredential] - $Credential, + [string] + $SettingsPath, [string] - $ConfigDirectory + $LogPath ) # Ensure the script is running with the necessary administrative/root privileges. @@ -129,30 +116,41 @@ function Register-PodeService { else { return $null } - # Define log paths and ensure the log directory exists - $LogPath = Join-Path -Path $MainScriptPath -ChildPath 'logs' - $LogFilePath = Join-Path -Path $LogPath -ChildPath "$($Name)_svc.log" + if (! $LogPath) { + $LogPath = Join-Path -Path $MainScriptPath -ChildPath 'logs' + } - if (-not (Test-Path $LogPath)) { + 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 ($ConfigDirectory) { - $settingsPath = Join-Path -Path $MainScriptPath -ChildPath $ConfigDirectory - if (! (Test-Path -Path $settingsPath -PathType Container)) { - $null = New-Item -Path $settingsPath -ItemType Directory - } + if (!$SettingsPath) { + $SettingsPath = Join-Path -Path $MainScriptPath -ChildPath 'svc_settings' } - else { - $settingsPath = $MainScriptPath + + if (! (Test-Path -Path $SettingsPath -PathType Container)) { + $null = New-Item -Path $settingsPath -ItemType Directory + } + + if (!$UserName) { + if ($IsWindows) { + $UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + } + else { + $UserName = [System.Environment]::UserName + } } - $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_srvsettings.json" - Write-PodeServiceLog -Message "Service '$Name' setting : $settingsFile." + + + $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_svcsettings.json" + Write-Verbose -Message "Service '$Name' setting : $settingsFile." # Generate the service settings JSON file $jsonContent = @{ @@ -161,8 +159,8 @@ function Register-PodeService { PwshPath = $PwshPath ParameterString = $ParameterString LogFilePath = $LogFilePath - Quiet = $Quiet - DisableTermination = $DisableTermination + Quiet = !$LogServicePodeHost.IsPresent + DisableTermination = $true ShutdownWaitTimeMs = $ShutdownWaitTimeMs Name = $Name } @@ -185,7 +183,7 @@ function Register-PodeService { StartupType = $StartupType BinPath = $binPath SettingsFile = $settingsFile - Credential = $Credential + Credential = if ($Password) { [pscredential]::new($UserName, $Password) }else { $null } SecurityDescriptorSddl = $SecurityDescriptorSddl OsArchitecture = "win-$osArchitecture" } @@ -228,6 +226,7 @@ function Register-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $false } } @@ -281,7 +280,7 @@ function Start-PodeService { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Running') { - Write-PodeServiceLog -Message "Service '$Name' started successfully." + Write-Verbose -Message "Service '$Name' started successfully." } else { throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $Name) @@ -289,7 +288,7 @@ function Start-PodeService { } else { # Log service is already running - Write-PodeServiceLog -Message "Service '$Name' is already running." + Write-Verbose -Message "Service '$Name' is already running." } } else { @@ -313,12 +312,12 @@ function Start-PodeService { } else { - Write-PodeServiceLog -Message "Service '$nameService' started successfully." + Write-Verbose -Message "Service '$nameService' started successfully." } } else { # Log service is already running - Write-PodeServiceLog -Message "Service '$nameService' is already running." + Write-Verbose -Message "Service '$nameService' is already running." } } else { @@ -338,12 +337,12 @@ function Start-PodeService { launchctl start "pode.$Name" # Log service started successfully - Write-PodeServiceLog -Message "Service '$Name' started successfully." + Write-Verbose -Message "Service '$Name' started successfully." return ($LASTEXITCODE -eq 0) } else { # Log service is already running - Write-PodeServiceLog -Message "Service '$Name' is already running." + Write-Verbose -Message "Service '$Name' is already running." } } else { @@ -354,6 +353,7 @@ function Start-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $false } return $true @@ -403,14 +403,14 @@ function Stop-PodeService { $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Stopped') { - Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + Write-Verbose -Message "Service '$Name' stopped successfully." } else { throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } } else { - Write-PodeServiceLog -Message "Service '$Name' is not running." + Write-Verbose -Message "Service '$Name' is not running." } } else { @@ -433,7 +433,7 @@ function Stop-PodeService { throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } else { - Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + Write-Verbose -Message "Service '$Name' stopped successfully." } } } @@ -452,11 +452,11 @@ function Stop-PodeService { # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { launchctl stop "pode.$Name" - Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + Write-Verbose -Message "Service '$Name' stopped successfully." return ($LASTEXITCODE -eq 0) } else { - Write-PodeServiceLog -Message "Service '$Name' is not running." + Write-Verbose -Message "Service '$Name' is not running." } } else { @@ -467,6 +467,7 @@ function Stop-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $false } return $true @@ -534,7 +535,7 @@ function Unregister-PodeService { $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Stopped') { - Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + Write-Verbose -Message "Service '$Name' stopped forcefully." } else { throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) @@ -550,10 +551,10 @@ function Unregister-PodeService { $null = Invoke-PodeWinElevatedCommand -Command 'Remove-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($null -ne $service) { - Write-PodeServiceLog -Message "Service '$Name' unregistered failed." + Write-Verbose -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } - Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + Write-Verbose -Message "Service '$Name' unregistered successfully." # Remove the service configuration if ($pathName) { @@ -567,6 +568,7 @@ function Unregister-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $false } } @@ -583,7 +585,7 @@ function Unregister-PodeService { # $status -eq 'active' if ($Force.IsPresent) { sudo systemctl stop $nameService - Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + Write-Verbose -Message "Service '$Name' stopped forcefully." } else { # Service is running. Use the -Force parameter to forcefully stop." @@ -608,12 +610,12 @@ function Unregister-PodeService { } sudo rm $serviceFilePath - Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + Write-Verbose -Message "Service '$Name' unregistered successfully." } sudo systemctl daemon-reload } else { - Write-PodeServiceLog -Message "Service '$Name' unregistered failed." + Write-Verbose -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } } @@ -625,6 +627,7 @@ function Unregister-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $false } } @@ -638,19 +641,19 @@ function Unregister-PodeService { # Check if the service has a PID entry if ($serviceInfo -match '"PID" = (\d+);') { launchctl stop "pode.$Name" - Write-PodeServiceLog -Message "Service '$Name' stopped successfully." + Write-Verbose -Message "Service '$Name' stopped successfully." $serviceIsRunning = ($LASTEXITCODE -ne 0) } else { $serviceIsRunning = $false - Write-PodeServiceLog -Message "Service '$Name' is not running." + Write-Verbose -Message "Service '$Name' is not running." } # Check if the service is running if ( $serviceIsRunning) { if ($Force.IsPresent) { launchctl stop "pode.$Name" - Write-PodeServiceLog -Message "Service '$Name' stopped forcefully." + Write-Verbose -Message "Service '$Name' stopped forcefully." } else { # Service is running. Use the -Force parameter to forcefully stop." @@ -679,7 +682,7 @@ function Unregister-PodeService { else { return $false } - Write-PodeServiceLog -Message "Service '$Name' unregistered successfully." + Write-Verbose -Message "Service '$Name' unregistered successfully." } else { # Service is not registered @@ -689,6 +692,7 @@ function Unregister-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $false } } @@ -820,6 +824,7 @@ function Get-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $null } } @@ -856,106 +861,8 @@ function Get-PodeService { } catch { $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception return $null } } -} - - -<# -.SYNOPSIS -Enables logging for the Pode service using a specified logging method. - -.DESCRIPTION -The `Enable-PodeServiceLogging` function configures and enables service logging for the Pode server using the provided logging method and specified log levels. It ensures that the logging method includes a valid script block and prevents duplicate logging methods from being enabled. - -.PARAMETER Method -A hashtable that defines the logging method. This should contain a `ScriptBlock` key, which specifies the script to be executed for logging. - -.PARAMETER Levels -An array of logging levels to capture. The available levels are 'Error', 'Warning', 'Informational', 'Verbose', 'Debug', or '*'. The default value is 'Error'. If '*' is specified, all levels are captured. - -.PARAMETER Raw -Indicates whether to log raw data without formatting. If set, the output is logged as-is without additional processing. - -.EXAMPLE -PS> Enable-PodeServiceLogging -Method @{ ScriptBlock = { Write-Host "Logging" } } -Levels 'Error', 'Warning' - -Enables error and warning level logging using the provided method. - -.EXAMPLE -PS> Enable-PodeServiceLogging -Method @{ ScriptBlock = { Write-Host "Raw Logging" } } -Raw - -Enables raw logging for all error levels. - -.NOTES -This function throws an error if the logging method has already been enabled or if the provided method does not include a valid script block. -#> -function Enable-PodeServiceLogging { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [hashtable] - $Method, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [ValidateSet('Error', 'Warning', 'Informational', 'Verbose', 'Debug', '*')] - [string[]] - $Levels = @('Error'), - - [switch] - $Raw - ) - - $name = Get-PodeServiceLoggingName - - # error if it's already enabled - if ($PodeContext.Server.Logging.Types.Contains($name)) { - # Error Logging has already been enabled - throw ($PodeLocale.errorLoggingAlreadyEnabledExceptionMessage) - } - - # ensure the Method contains a scriptblock - if (Test-PodeIsEmpty $Method.ScriptBlock) { - # The supplied output Method for Error Logging requires a valid ScriptBlock - throw ($PodeLocale.loggingMethodRequiresValidScriptBlockExceptionMessage -f 'Error') - } - - # all errors? - if ($Levels -contains '*') { - $Levels = @('Error', 'Warning', 'Informational', 'Verbose', 'Debug') - } - - # add the error logger - $PodeContext.Server.Logging.Types[$name] = @{ - Method = $Method - ScriptBlock = (Get-PodeLoggingInbuiltType -Type Errors) - Arguments = @{ - Raw = $Raw - Levels = $Levels - } - } -} - -<# -.SYNOPSIS -Disables the logging for the Pode service. - -.DESCRIPTION -The `Disable-PodeServiceLogging` function disables the currently enabled logging method for the Pode service. It removes the logger associated with the service by using the logger's name. - -.EXAMPLE -PS> Disable-PodeServiceLogging - -Disables the service logging for Pode. - -.NOTES -This function uses the `Remove-PodeLogger` cmdlet to remove the logger by name. -#> -function Disable-PodeServiceLogging { - [CmdletBinding()] - param() - - Remove-PodeLogger -Name (Get-PodeServiceLoggingName) -} +} \ No newline at end of file diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 893ffd464..e285c6bd0 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -831,29 +831,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. .EXAMPLE 'Some output' | Write-PodeHost -ForegroundColor Cyan @@ -863,6 +866,7 @@ function Write-PodeHost { [CmdletBinding(DefaultParameterSetName = 'inbuilt')] param( [Parameter(Position = 0, ValueFromPipeline = $true)] + [Alias('Message')] [object] $Object, @@ -883,7 +887,10 @@ function Write-PodeHost { [Parameter( Mandatory = $false, ParameterSetName = 'object')] [string] - $Label + $Label, + + [switch] + $Force ) begin { # Initialize an array to hold piped-in values @@ -896,7 +903,7 @@ function Write-PodeHost { } end { - if ($PodeContext.Server.Quiet) { + if ($PodeContext.Server.Quiet -and !($Force.IsPresent)) { return } # Set Object to the array of values From 00c525226bb7b7ade786fb707c1c89f5d7f0de8b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 22 Oct 2024 09:57:47 -0700 Subject: [PATCH 22/93] remove spaces between function Register-PodeService and header --- src/Public/Service.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 681c22f21..1c9fbb85c 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -57,8 +57,6 @@ - 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)] From 302bb09f8110d69552bca4dfd3de9a95752d14c3 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 22 Oct 2024 12:49:32 -0700 Subject: [PATCH 23/93] reinstated create user --- src/Locales/ar/Pode.psd1 | 1 + src/Locales/de/Pode.psd1 | 1 + src/Locales/en-us/Pode.psd1 | 1 + src/Locales/en/Pode.psd1 | 1 + src/Locales/es/Pode.psd1 | 1 + src/Locales/fr/Pode.psd1 | 1 + src/Locales/it/Pode.psd1 | 1 + src/Locales/ja/Pode.psd1 | 5 +++-- src/Locales/ko/Pode.psd1 | 5 +++-- src/Locales/nl/Pode.psd1 | 5 +++-- src/Locales/pl/Pode.psd1 | 5 +++-- src/Locales/pt/Pode.psd1 | 5 +++-- src/Locales/zh/Pode.psd1 | 5 +++-- src/Private/Service.ps1 | 8 ++++---- src/Public/Service.ps1 | 34 ++++++++++++++++++++++------------ 15 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 8df8789e0..1f005ee04 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -298,4 +298,5 @@ serviceRegistrationException = "فشل تسجيل الخدمة '{0}'." serviceIsRunningException = "الخدمة '{0}' تعمل. استخدم المعامل -Force للإيقاف بالقوة." serviceUnRegistrationException = "فشل إلغاء تسجيل الخدمة '{0}'." + passwordRequiredForServiceUserException = "مطلوب كلمة مرور عند تحديد مستخدم الخدمة في نظام Windows. يرجى تقديم كلمة مرور صالحة للمستخدم '{0}'." } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index 1697e8cd3..1d5e8f355 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -298,4 +298,5 @@ 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." } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index f856d8d2c..d22be7af6 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -298,4 +298,5 @@ 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}'." } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index b73bbe17d..5ab1cde07 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -298,4 +298,5 @@ 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}'." } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 66d5ed778..1ae18d9fb 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -298,4 +298,5 @@ 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}'." } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 840d0b28c..1cca19bcc 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -298,4 +298,5 @@ 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}'." } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 843dd6429..03fdad7ea 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -298,4 +298,5 @@ 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}'." } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index b717cc7f0..444e4c91e 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -293,9 +293,10 @@ unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' LocalEndpointConflictExceptionMessage = "'{0}' と '{1}' は OpenAPI のローカルエンドポイントとして定義されていますが、API 定義ごとに 1 つのローカルエンドポイントのみ許可されます。" serviceAlreadyRegisteredException = "サービス '{0}' はすでに登録されています。" - serviceIsNotRegisteredException = "サービス '{0}' は登録されていません。" + serviceIsNotRegisteredException = "サービス '{0}' は登録されていません。" serviceCommandFailedException = "サービスコマンド '{0}' はサービス '{1}' で失敗しました。" - serviceRegistrationException = "サービス '{0}' の登録に失敗しました。" + serviceRegistrationException = "サービス '{0}' の登録に失敗しました。" serviceIsRunningException = "サービス '{0}' が実行中です。強制的に停止するには、-Force パラメーターを使用してください。" serviceUnRegistrationException = "サービス '{0}' の登録解除に失敗しました。" + passwordRequiredForServiceUserException = "Windowsでサービスユーザーを指定する際にはパスワードが必要です。ユーザー '{0}' に有効なパスワードを入力してください。" } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index b842d3f97..0ad490654 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -293,9 +293,10 @@ unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' LocalEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." serviceAlreadyRegisteredException = "서비스 '{0}'가 이미 등록되었습니다." - serviceIsNotRegisteredException = "서비스 '{0}'가 등록되지 않았습니다." + serviceIsNotRegisteredException = "서비스 '{0}'가 등록되지 않았습니다." serviceCommandFailedException = "서비스 명령 '{0}' 이(가) 서비스 '{1}' 에서 실패했습니다." - serviceRegistrationException = "서비스 '{0}' 등록에 실패했습니다." + serviceRegistrationException = "서비스 '{0}' 등록에 실패했습니다." serviceIsRunningException = "서비스 '{0}'가 실행 중입니다. 강제로 중지하려면 -Force 매개변수를 사용하세요." serviceUnRegistrationException = "서비스 '{0}' 등록 취소에 실패했습니다." + passwordRequiredForServiceUserException = "Windows에서 서비스 사용자를 지정할 때는 비밀번호가 필요합니다. 사용자 '{0}'에 대해 유효한 비밀번호를 입력하세요." } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 8ffa6a0b7..e6c890c2e 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -293,9 +293,10 @@ 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." + 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." + 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}'." } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index e08c36420..c759f3144 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -293,9 +293,10 @@ 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." + 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ę." + 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}'." } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index cd9c39a3d..f90657470 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -293,9 +293,10 @@ 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." + 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}'." + 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}'." } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 986de7cfa..53b35f424 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -293,9 +293,10 @@ unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' LocalEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" serviceAlreadyRegisteredException = "服务 '{0}' 已经注册。" - serviceIsNotRegisteredException = "服务 '{0}' 未注册。" + serviceIsNotRegisteredException = "服务 '{0}' 未注册。" serviceCommandFailedException = "服务命令 '{0}' 在服务 '{1}' 上失败。" - serviceRegistrationException = "服务 '{0}' 注册失败。" + serviceRegistrationException = "服务 '{0}' 注册失败。" serviceIsRunningException = "服务 '{0}' 正在运行。使用 -Force 参数强制停止。" serviceUnRegistrationException = "服务 '{0}' 的注销失败。" + passwordRequiredForServiceUserException = "在 Windows 中指定服务用户时需要密码。请为用户 '{0}' 提供有效的密码。" } \ No newline at end of file diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 322c6cfb7..2a0cf1bfe 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -290,8 +290,8 @@ function Register-PodeMacService { .PARAMETER Group The group under which the service will run. Defaults to the same as the `User` parameter. -.PARAMETER SkipUserCreation - A switch to skip the creation of the user if it does not exist. +.PARAMETER CreateUser + A switch create the user if it does not exist. .PARAMETER OsArchitecture The architecture of the operating system (e.g., `x64`, `arm64`). Used to locate the appropriate binary. @@ -345,7 +345,7 @@ function Register-PodeLinuxService { $Start, [switch] - $SkipUserCreation, + $CreateUser, [string] $OsArchitecture @@ -386,7 +386,7 @@ WantedBy=multi-user.target Remove-Item -path $tempFile -ErrorAction SilentlyContinue # Create user if needed - if (!$SkipUserCreation.IsPresent) { + if ($CreateUser.IsPresent) { # Run the id command to check if the user exists id $User 2>&1 if ($LASTEXITCODE -ne 0) { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 1c9fbb85c..c98a54119 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -31,6 +31,9 @@ .PARAMETER UserName Specifies the username under which the service will run by default is the current user. +.PARAMETER CreateUser + A switch create the user if it does not exist (Linux Only). + .PARAMETER Start A switch to start the service immediately after registration. @@ -88,6 +91,9 @@ function Register-PodeService { [string] $UserName, + [switch] + $CreateUser, + [switch] $Start, @@ -137,7 +143,12 @@ function Register-PodeService { $null = New-Item -Path $settingsPath -ItemType Directory } - if (!$UserName) { + if ([string]::IsNullOrEmpty($UserName)) { + if ($IsWindows -and ($null -eq $Password)) { + throw ($Podelocale.passwordRequiredForServiceUserException -f $UserName) + } + } + else { if ($IsWindows) { $UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name } @@ -146,9 +157,8 @@ function Register-PodeService { } } - $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_svcsettings.json" - Write-Verbose -Message "Service '$Name' setting : $settingsFile." + Write-Verbose -Message "Service '$Name' setting : $settingsFile." # Generate the service settings JSON file $jsonContent = @{ @@ -189,15 +199,15 @@ function Register-PodeService { } elseif ($IsLinux) { $param = @{ - Name = $Name - Description = $Description - BinPath = $binPath - SettingsFile = $settingsFile - User = $User - Group = $Group - Start = $Start - SkipUserCreation = $SkipUserCreation - OsArchitecture = "linux-$osArchitecture" + Name = $Name + Description = $Description + BinPath = $binPath + SettingsFile = $settingsFile + User = $User + Group = $Group + Start = $Start + CreateUser = $CreateUser + OsArchitecture = "linux-$osArchitecture" } $operation = Register-PodeLinuxService @param } From a67b3c3570d45a004d56d0e9bdf8d4d6c75fd98a Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 22 Oct 2024 13:30:39 -0700 Subject: [PATCH 24/93] fixes --- .gitignore | 3 +- examples/HelloService/HelloService.ps1 | 7 +- examples/HelloService/HelloServices.ps1 | 143 ++++++++++++++++++ .../Hello Service2_svcsettings.json | 12 -- src/Public/Service.ps1 | 15 +- 5 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 examples/HelloService/HelloServices.ps1 delete mode 100644 examples/HelloService/svc_settings/Hello Service2_svcsettings.json diff --git a/.gitignore b/.gitignore index 0c6a11c0f..c0662ab79 100644 --- a/.gitignore +++ b/.gitignore @@ -266,4 +266,5 @@ examples/PetStore/data/PetData.json packers/choco/pode.nuspec packers/choco/tools/ChocolateyInstall.ps1 docs/Getting-Started/Samples.md -examples/*_srvsettings.json +examples/HelloService/*_svcsettings.json +examples/HelloService/svc_settings diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 14d3f09c9..c7abe2bcd 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -61,6 +61,9 @@ [CmdletBinding(DefaultParameterSetName = 'Inbuilt')] param( + [Parameter( ParameterSetName = 'Inbuilt')] + [int] + $Port = 8080, [Parameter(Mandatory = $true, ParameterSetName = 'Register')] [switch] $Register, @@ -103,7 +106,7 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service2' + Register-PodeService -Name 'Hello Service2' -ParameterString '-Port 8081' exit } if ( $Unregister.IsPresent) { @@ -130,7 +133,7 @@ Start-PodeServer { New-PodeLoggingMethod -File -Name 'errors' -MaxDays 4 -Path './logs' | Enable-PodeErrorLogging # Add an HTTP endpoint listening on localhost at port 8080 - Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http + Add-PodeEndpoint -Address localhost -Port $Port -Protocol Http # Add a route for GET requests to the root path '/' Add-PodeRoute -Method Get -Path '/' -ScriptBlock { diff --git a/examples/HelloService/HelloServices.ps1 b/examples/HelloService/HelloServices.ps1 new file mode 100644 index 000000000..43f4167eb --- /dev/null +++ b/examples/HelloService/HelloServices.ps1 @@ -0,0 +1,143 @@ +<# +.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 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. + +.EXAMPLE + Register all services: + ./script.ps1 -Register + +.EXAMPLE + Start all services: + ./script.ps1 -Start + +.EXAMPLE + Query the status of all services: + ./script.ps1 -Query + +.EXAMPLE + Stop all services: + ./script.ps1 -Stop + +.EXAMPLE + Forcefully unregister all services: + ./script.ps1 -Unregister -Force + +.LINK + https://example.com + +.NOTES + Author: Your Name + License: MIT License +#> + + +[CmdletBinding(DefaultParameterSetName = 'Inbuilt')] +param( + [Parameter( ParameterSetName = 'Inbuilt')] + [int] + $Port = 8080, + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] + [switch] + $Register, + [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 +) +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) { + $services.GetEnumerator() | ForEach-Object { Register-PodeService -Name $($_.Key) -ParameterString "-Port $($_.Value)" } + exit +} +if ( $Unregister.IsPresent) { + $services.GetEnumerator() | ForEach-Object { try{Unregister-PodeService -Name $($_.Key) -Force:$Force }catch{Write-Error -Exception $_.Exception}} + exit +} +if ($Start.IsPresent) { + $services.GetEnumerator() | ForEach-Object { Start-PodeService -Name $($_.Key) } + exit +} + +if ($Stop.IsPresent) { + $services.GetEnumerator() | ForEach-Object { Stop-PodeService -Name $($_.Key) } + exit +} + +if ($Query.IsPresent) { + $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) } + exit +} + +# Start the Pode server +Start-PodeServer { + New-PodeLoggingMethod -File -Name "errors-$port" -MaxDays 4 -Path './logs' | Enable-PodeErrorLogging + + # 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/svc_settings/Hello Service2_svcsettings.json b/examples/HelloService/svc_settings/Hello Service2_svcsettings.json deleted file mode 100644 index 9f48d3e53..000000000 --- a/examples/HelloService/svc_settings/Hello Service2_svcsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "PodePwshWorker": { - "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\logs\\Hello Service2_svc.log", - "ParameterString": "", - "Name": "Hello Service2", - "ShutdownWaitTimeMs": 30000, - "ScriptPath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloService\\HelloService.ps1", - "Quiet": true, - "PwshPath": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", - "DisableTermination": true - } -} diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index c98a54119..1adda264b 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -144,17 +144,20 @@ function Register-PodeService { } if ([string]::IsNullOrEmpty($UserName)) { - if ($IsWindows -and ($null -eq $Password)) { - throw ($Podelocale.passwordRequiredForServiceUserException -f $UserName) - } - } - else { if ($IsWindows) { - $UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + if ( ($null -ne $Password)) { + $UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + } } else { $UserName = [System.Environment]::UserName } + + } + else { + if ($IsWindows -and ($null -eq $Password)) { + throw ($Podelocale.passwordRequiredForServiceUserException -f $UserName) + } } $settingsFile = Join-Path -Path $settingsPath -ChildPath "$($Name)_svcsettings.json" From afcd5c1bf7125cf2fe7af800c8246c72aa70dd8a Mon Sep 17 00:00:00 2001 From: Max Daneri Date: Tue, 22 Oct 2024 17:57:54 -0400 Subject: [PATCH 25/93] fixes --- src/Private/Service.ps1 | 60 +++++++++++++++--- src/Public/Service.ps1 | 131 +++++++++++++++++++--------------------- 2 files changed, 116 insertions(+), 75 deletions(-) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 2a0cf1bfe..ac3d4ee56 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -257,6 +257,7 @@ function Register-PodeMacService { catch { $_ | Write-PodeErrorLog throw $_ # Rethrow the error after logging + return $false } return $true @@ -351,7 +352,7 @@ function Register-PodeLinuxService { $OsArchitecture ) $nameService = "$Name.service".Replace(' ', '_') - systemctl status $nameService 2>&1 + $null = systemctl status $nameService 2>&1 # Check if the service is already registered if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { @@ -360,6 +361,8 @@ function Register-PodeLinuxService { } # Create a temporary file $tempFile = [System.IO.Path]::GetTempFileName() + + $execStart = "$BinPath/$OsArchitecture/PodeMonitor `"$SettingsFile`"" # Create the service file @" [Unit] @@ -367,7 +370,7 @@ Description=$Description After=network.target [Service] -ExecStart=$BinPath/$OsArchitecture/PodeMonitor "$SettingsFile" +ExecStart=$execStart WorkingDirectory=$BinPath Restart=always User=$User @@ -379,7 +382,7 @@ User=$User WantedBy=multi-user.target "@ | Set-Content -Path $tempFile -Encoding UTF8 - Write-Verbose -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + Write-Verbose -Message "Service '$nameService' ExecStart : $execStart)." sudo cp $tempFile "/etc/systemd/system/$nameService" @@ -397,8 +400,7 @@ WantedBy=multi-user.target # Enable the service and check if it fails try { - sudo systemctl enable $nameService 2>&1 - if ($LASTEXITCODE -ne 0) { + if (!(Enable-PodeLinuxService -Name $nameService)) { # Service registration failed. throw ($PodeLocale.serviceRegistrationException -f $nameService) } @@ -406,6 +408,7 @@ WantedBy=multi-user.target catch { $_ | Write-PodeErrorLog throw $_ # Rethrow the error after logging + return $false } return $true @@ -537,6 +540,7 @@ function Register-PodeWindowsService { catch { $_ | Write-PodeErrorLog throw $_ # Rethrow the error after logging + return $false } return $true @@ -596,7 +600,49 @@ function Confirm-PodeAdminPrivilege { } +function Test-PodeLinuxServiceIsRegistered { + param( + $Name + ) + $systemctlStatus = systemctl status $Name 2>&1 + $isRegistered = ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) + Write-Verbose -Message ($systemctlStatus -join '`n') + return $isRegistered +} + +function Test-PodeLinuxServiceIsActive { + param( + $Name + ) + $systemctlIsActive = systemctl is-active $Name 2>&1 + $isActive = $systemctlIsActive -eq 'active' + Write-Verbose -Message ($systemctlIsActive -join '`n') + return $isActive +} + + +function Disable-PodeLinuxService { + param( + $Name + ) + $systemctlDisable = sudo systemctl disable $Name 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($systemctlDisable -join '`n') + return $success +} + -function Get-PodeServiceLoggingName { - return '__pode_log_service__' +function Enable-PodeLinuxService { + param( + $Name + ) + $systemctlEnable = sudo systemctl enable $Name 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($systemctlEnable -join '`n') + return $success } + + + + + diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 1adda264b..225e16dbd 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -311,14 +311,11 @@ function Start-PodeService { elseif ($IsLinux) { $nameService = "$Name.service".Replace(' ', '_') # Check if the service exists - systemctl status $nameService 2>&1 - if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { + if ((Test-PodeLinuxServiceIsRegistered $nameService)) { # Check if the service is already running - $status = systemctl is-active $nameService - if ($status -ne 'active') { + if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { sudo systemctl start $nameService - $status = systemctl is-active $nameService - if ($status -ne 'active') { + if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $nameService) } else { @@ -332,6 +329,7 @@ function Start-PodeService { } } else { + Write-Verbose -Message $systemctlStatus # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) } @@ -431,28 +429,24 @@ function Stop-PodeService { } elseif ($IsLinux) { $nameService = "$Name.service".Replace(' ', '_') - systemctl status $nameService 2>&1 # Check if the service is already registered - if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { - # Check if the service exists - if (systemctl status $nameService -q) { - $status = systemctl is-active $nameService - if ($status -eq 'active') { - sudo systemctl stop $nameService - $status = systemctl is-active $nameService - if ($status -eq 'active') { - throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) - } - else { - Write-Verbose -Message "Service '$Name' stopped successfully." - } + if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { + # Check if the service is active + if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { + sudo systemctl stop $nameService + if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { + throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) + } + else { + Write-Verbose -Message "Service '$Name' stopped successfully." } - } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) } } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) + } + } elseif ($IsMacOS) { # Check if the service exists in launchctl @@ -587,13 +581,10 @@ function Unregister-PodeService { elseif ($IsLinux) { try { $nameService = "$Name.service".Replace(' ', '_') - systemctl status $nameService 2>&1 # Check if the service is already registered - if ($LASTEXITCODE -eq 0 -or $LASTEXITCODE -eq 3) { - # Check if the service is running - $status = systemctl is-active $nameService 2>&1 - if ($status -eq 'active') { - # $status -eq 'active' + if ((Test-PodeLinuxServiceIsRegistered $nameService)) { + # Check if the service is active + if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { if ($Force.IsPresent) { sudo systemctl stop $nameService Write-Verbose -Message "Service '$Name' stopped forcefully." @@ -603,8 +594,7 @@ function Unregister-PodeService { throw ($Podelocale.serviceIsRunningException -f $nameService) } } - sudo systemctl disable $nameService - if ($LASTEXITCODE -eq 0 ) { + if ((Disable-PodeLinuxService -Name $nameService)) { # Read the content of the service file $serviceFilePath = "/etc/systemd/system/$nameService" if ((Test-path -path $serviceFilePath -PathType Leaf)) { @@ -783,7 +773,7 @@ function Get-PodeService { } } else { - #Write-PodeErrorLog -Message "Service '$Name' not found on Windows." + Write-Verbose -Message "Service '$Name' not found." return $null } } @@ -792,43 +782,48 @@ function Get-PodeService { try { $nameService = "$Name.service".Replace(' ', '_') # Check if the service exists on Linux (systemd) - $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}') - $status = 'Running' - } - '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 = 'Paused' - } - 'inactive' { - $status = 'Stopped' - } - 'failed' { - $status = 'Stopped' - } - 'activating' { - $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') - $status = 'Starting' - } - 'deactivating' { - $status = 'Stopping' + 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}') + $status = 'Running' + } + '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 = 'Paused' + } + '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' + } } - default { - $status = 'Stopped' + return @{ + Name = $Name + Status = $status + Pid = $servicePid } } - return @{ - Name = $Name - Status = $status - Pid = $servicePid + else { + Write-Verbose -Message "Service '$nameService' not found." } @@ -866,7 +861,7 @@ function Get-PodeService { } } else { - Write-PodeErrorLog -Message "Service 'pode.$Name' not found on macOS." + Write-Verbose -Message "Service '$Name' not found." return $null } } From 9326e32b10d37a3a3879f0b92040f04e3662daf8 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 22 Oct 2024 15:06:05 -0700 Subject: [PATCH 26/93] fix the example --- examples/HelloService/HelloService.ps1 | 16 +++++++++++----- examples/HelloService/HelloServices.ps1 | 20 +++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index c7abe2bcd..935239cde 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -33,23 +33,23 @@ .EXAMPLE Register the service: - ./HelloWorld.ps1 -Register + ./HelloService.ps1 -Register .EXAMPLE Start the service: - ./HelloWorld.ps1 -Start + ./HelloService.ps1 -Start .EXAMPLE Query the service: - ./HelloWorld.ps1 -Query + ./HelloService.ps1 -Query .EXAMPLE Stop the service: - ./HelloWorld.ps1 -Stop + ./HelloService.ps1 -Stop .EXAMPLE Unregister the service: - ./HelloWorld.ps1 -Unregister -Force + ./HelloService.ps1 -Unregister -Force .LINK https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloService.ps1 @@ -64,21 +64,27 @@ param( [Parameter( ParameterSetName = 'Inbuilt')] [int] $Port = 8080, + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] [switch] $Register, + [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 diff --git a/examples/HelloService/HelloServices.ps1 b/examples/HelloService/HelloServices.ps1 index 43f4167eb..c534bb6c9 100644 --- a/examples/HelloService/HelloServices.ps1 +++ b/examples/HelloService/HelloServices.ps1 @@ -28,29 +28,29 @@ .EXAMPLE Register all services: - ./script.ps1 -Register + ./HelloServices.ps1 -Register .EXAMPLE Start all services: - ./script.ps1 -Start + ./HelloServices.ps1 -Start .EXAMPLE Query the status of all services: - ./script.ps1 -Query + ./HelloServices.ps1 -Query .EXAMPLE Stop all services: - ./script.ps1 -Stop + ./HelloServices.ps1 -Stop .EXAMPLE Forcefully unregister all services: - ./script.ps1 -Unregister -Force + ./HelloServices.ps1 -Unregister -Force .LINK - https://example.com + https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloServices.ps1 .NOTES - Author: Your Name + Author: Pode Team License: MIT License #> @@ -60,21 +60,27 @@ param( [Parameter( ParameterSetName = 'Inbuilt')] [int] $Port = 8080, + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] [switch] $Register, + [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 From 78e50f278e3f73d71c9b6abe3507ffb07e13c953 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 23 Oct 2024 11:15:23 -0700 Subject: [PATCH 27/93] minor log fixex --- src/PodePwshMonitor/PodePwshMonitor.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/PodePwshMonitor/PodePwshMonitor.cs b/src/PodePwshMonitor/PodePwshMonitor.cs index 55d925a43..12b3180df 100644 --- a/src/PodePwshMonitor/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/PodePwshMonitor.cs @@ -57,7 +57,7 @@ public class PodePwshMonitor public PodePwshMonitor(string scriptPath, string pwshPath, string parameterString = "", string logFilePath = ".\\PodePwshMonitorService.log", bool quiet = true, bool disableTermination = true, int shutdownWaitTimeMs = 30000) { - Console.WriteLine("logFilePath{0}", logFilePath); + Console.WriteLine("logFilePath{0}", logFilePath); // Initialize fields with constructor arguments _scriptPath = scriptPath; // Path to the PowerShell script to be executed _pwshPath = pwshPath; // Path to the PowerShell executable (pwsh) @@ -89,10 +89,14 @@ public void StartPowerShellProcess() CreateNoWindow = true // Do not create a new window } }; - + Log($"[Server] - Starting ..."); // Properly escape double quotes within the JSON string string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; - + Log($"[Server] - Powershell path {_pwshPath}"); + Log($"[Server] - PodeService content:"); + Log($"[Server] - DisableTermination\t= {_disableTermination.ToString().ToLower()}"); + Log($"[Server] - Quiet\t= {_quiet.ToString().ToLower()}"); + Log($"[Server] - PipeName\t= {_pipeName}"); // Build the PowerShell command with NoProfile and global variable initialization string command = $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; @@ -202,6 +206,7 @@ public void StopPowerShellProcess() _pipeClient = null; } Log("[Server] - PowerShell process and pipe client disposed."); + Log("[Server] - Done."); } } From fc3e1c3db9133ac88b2a55d8b51d7e41b14d19dc Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 23 Oct 2024 15:50:03 -0700 Subject: [PATCH 28/93] MacOS improvements --- src/Private/Service.ps1 | 313 ++++++++++++++++++++++++++++++++++++++-- src/Public/Service.ps1 | 180 +++++++++++++---------- 2 files changed, 406 insertions(+), 87 deletions(-) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index ac3d4ee56..b446e2082 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -194,10 +194,12 @@ function Register-PodeMacService { $LogPath ) + $nameService = "pode.$Name.service".Replace(' ', '_') + # Check if the service is already registered - if (launchctl list | Select-String "pode.$Name") { + if ((Test-PodeMacOsServiceIsRegistered $nameService)) { # Service is already registered. - throw ($PodeLocale.serviceAlreadyRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceAlreadyRegisteredException -f $nameService) } # Determine whether the service should run at load @@ -210,7 +212,7 @@ function Register-PodeMacService { Label - pode.$Name + $nameService ProgramArguments @@ -225,10 +227,10 @@ function Register-PodeMacService { $runAtLoad StandardOutPath - $LogPath/pode.$Name.stdout.log + $LogPath/$nameService.stdout.log StandardErrorPath - $LogPath/pode.$Name.stderr.log + $LogPath/$nameService.stderr.log KeepAlive @@ -237,21 +239,20 @@ function Register-PodeMacService { -"@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" -Encoding UTF8 +"@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -Encoding UTF8 - Write-Verbose -Message "Service '$Name' BinaryPathName : $($params['BinaryPathName'])." + Write-Verbose -Message "Service '$nameService' WorkingDirectory : $($BinPath)." - chmod +r "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" + chmod +r "$($HOME)/Library/LaunchAgents/$($nameService).plist" try { # Load the plist with launchctl - launchctl load "$($HOME)/Library/LaunchAgents/pode.$($Name).plist" + launchctl load "$($HOME)/Library/LaunchAgents/$($nameService).plist" # Verify the service is now registered - if (! (launchctl list | Select-String "pode.$Name")) { + if (! (Test-PodeMacOsServiceIsRegistered $nameService)) { # Service registration failed. - throw ($PodeLocale.serviceRegistrationException -f "pode.$Name") - + throw ($PodeLocale.serviceRegistrationException -f $nameService) } } catch { @@ -599,7 +600,24 @@ function Confirm-PodeAdminPrivilege { } } +<# + .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( $Name @@ -610,6 +628,24 @@ function Test-PodeLinuxServiceIsRegistered { 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( $Name @@ -620,7 +656,24 @@ function Test-PodeLinuxServiceIsActive { 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( $Name @@ -631,7 +684,24 @@ function Disable-PodeLinuxService { 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( $Name @@ -642,7 +712,226 @@ function Enable-PodeLinuxService { 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( + $Name + ) + $serviceStopInfo = sudo systemctl stop $Name 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( + $Name + ) + $serviceStartInfo = sudo systemctl start $Name 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. + + .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( + $Name + ) + $systemctlStatus = launchctl list $Name 2>&1 + $isRegistered = ($LASTEXITCODE -eq 0) + Write-Verbose -Message ($systemctlStatus -join '`n') + return $isRegistered +} +<# + .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( + $Name + ) + $serviceInfo = launchctl list $name + $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( + $Name + ) + $serviceInfo = launchctl list $name + $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. + + .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( + $Name + ) + $systemctlDisable = launchctl unload "$HOME/Library/LaunchAgents/$Name.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. + + .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( + $Name + ) + $serviceStopInfo = launchctl stop $Name 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($serviceStopInfo -join "`n") + return $success +} + +<# + .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. + + .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 { + param( + $Name + ) + $serviceStartInfo = launchctl start $Name 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($serviceStartInfo -join "`n") + return $success +} \ No newline at end of file diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 225e16dbd..e564e702e 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -314,14 +314,16 @@ function Start-PodeService { if ((Test-PodeLinuxServiceIsRegistered $nameService)) { # Check if the service is already running if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { - sudo systemctl start $nameService - if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { - throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $nameService) - } - else { - - Write-Verbose -Message "Service '$nameService' started successfully." + # Start the service + if ((Start-PodeLinuxService -Name $nameService)) { + # Check if the service is active + if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$nameService' started successfully." + return $true + } } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $nameService) } else { # Log service is already running @@ -329,34 +331,36 @@ function Start-PodeService { } } else { - Write-Verbose -Message $systemctlStatus # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) } } elseif ($IsMacOS) { - # Check if the service exists in launchctl - if (launchctl list | Select-String "pode.$Name") { - - $serviceInfo = launchctl list "pode.$Name" -join "`n" - - # Check if the service has a PID entry - if (!($serviceInfo -match '"PID" = (\d+);')) { - launchctl start "pode.$Name" - - # Log service started successfully - Write-Verbose -Message "Service '$Name' started successfully." - return ($LASTEXITCODE -eq 0) + $nameService = "pode.$Name.service".Replace(' ', '_') + # Check if the service exists + if ((Test-PodeMacOsServiceIsRegistered $nameService)) { + # Check if the service is already running + if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { + # Start the service + if ((Start-PodeMacOsService -Name $nameService)) { + # Check if the service is active + if ((Test-PodeMacOsServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$nameService' started successfully." + return $true + } + } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $nameService) } else { # Log service is already running - Write-Verbose -Message "Service '$Name' is already running." + Write-Verbose -Message "Service '$nameService' is already running." } } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) } } } @@ -415,6 +419,7 @@ function Stop-PodeService { Write-Verbose -Message "Service '$Name' stopped successfully." } else { + # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } } @@ -433,13 +438,20 @@ function Stop-PodeService { if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { # Check if the service is active if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { - sudo systemctl stop $nameService - if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { - throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) - } - else { - Write-Verbose -Message "Service '$Name' stopped successfully." + #Stop the service + if (( Stop-PodeLinuxService -Name $nameService)) { + # Check if the service is active + if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + return $true + } } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo launchctl stop', $Name) + } + else { + Write-Verbose -Message "Service '$Name' is not running." } } else { @@ -449,16 +461,21 @@ function Stop-PodeService { } elseif ($IsMacOS) { - # Check if the service exists in launchctl - if (launchctl list | Select-String "pode.$Name") { - # Stop the service if running - $serviceInfo = launchctl list "pode.$Name" -join "`n" + $nameService = "pode.$Name.service".Replace(' ', '_') + # Check if the service is already registered + if ((Test-PodeMacOsServiceIsRegistered -Name $nameService)) { + # Check if the service is active + if ((Test-PodeMacOsServiceIsActive $nameService)) { + if ((Stop-PodeMacOsService $nameService)) { + if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + return $true + } + } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) - # Check if the service has a PID entry - if ($serviceInfo -match '"PID" = (\d+);') { - launchctl stop "pode.$Name" - Write-Verbose -Message "Service '$Name' stopped successfully." - return ($LASTEXITCODE -eq 0) } else { Write-Verbose -Message "Service '$Name' is not running." @@ -466,7 +483,7 @@ function Stop-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) } } } @@ -543,12 +560,13 @@ function Unregister-PodeService { Write-Verbose -Message "Service '$Name' stopped forcefully." } else { + # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } } else { # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f "pode.$Name") + throw ($Podelocale.serviceIsRunningException -f $nameService ) } } @@ -586,8 +604,21 @@ function Unregister-PodeService { # Check if the service is active if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { if ($Force.IsPresent) { - sudo systemctl stop $nameService - Write-Verbose -Message "Service '$Name' stopped forcefully." + #Stop the service + if (( Stop-PodeLinuxService -Name $nameService)) { + # Check if the service is active + if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + } + else { + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl stop', $Name) + } + } + else { + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl stop', $Name) + } } else { # Service is running. Use the -Force parameter to forcefully stop." @@ -622,7 +653,7 @@ function Unregister-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) } return $true } @@ -635,36 +666,36 @@ function Unregister-PodeService { elseif ($IsMacOS) { try { - # Check if the service exists - - if (launchctl list | Select-String "pode.$Name") { - $serviceInfo = (launchctl list "pode.$Name") -join "`n" - # Check if the service has a PID entry - if ($serviceInfo -match '"PID" = (\d+);') { - launchctl stop "pode.$Name" - Write-Verbose -Message "Service '$Name' stopped successfully." - $serviceIsRunning = ($LASTEXITCODE -ne 0) - } - else { - $serviceIsRunning = $false - Write-Verbose -Message "Service '$Name' is not running." - } - - # Check if the service is running - if ( $serviceIsRunning) { + $nameService = "pode.$Name.service".Replace(' ', '_') + # Check if the service is already registered + if (Test-PodeMacOsServiceIsRegistered $nameService) { + # Check if the service is active + if ((Test-PodeMacOsServiceIsActive -Name $nameService)) { if ($Force.IsPresent) { - launchctl stop "pode.$Name" - Write-Verbose -Message "Service '$Name' stopped forcefully." + #Stop the service + if (( Stop-PodeMacOsService -Name $nameService)) { + # Check if the service is active + if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + } + else { + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) + } + } + else { + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) + } } else { # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f "$Name") + throw ($Podelocale.serviceIsRunningException -f $nameService) } } - launchctl unload "$HOME/Library/LaunchAgents/pode.$Name.plist" - if ($LASTEXITCODE -eq 0) { - $plistFilePath = "$HOME/Library/LaunchAgents/pode.$Name.plist" + if ((Disable-PodeMacOsService -Name $nameService)) { + $plistFilePath = "$HOME/Library/LaunchAgents/$nameService.plist" #Check if the plist file exists if (Test-Path -Path $plistFilePath) { # Read the content of the plist file @@ -678,16 +709,18 @@ function Unregister-PodeService { } Remove-Item -Path $plistFilePath -ErrorAction Break + + Write-Verbose -Message "Service '$Name' unregistered successfully." } } else { - return $false + Write-Verbose -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) } - Write-Verbose -Message "Service '$Name' unregistered successfully." } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "pode.$Name") + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) } return $true } @@ -837,15 +870,12 @@ function Get-PodeService { elseif ($IsMacOS) { try { + $nameService = "pode.$Name.service".Replace(' ', '_') # Check if the service exists on macOS (launchctl) - $serviceList = launchctl list | Select-String "pode.$Name" - if ($serviceList) { - $serviceInfo = launchctl list "pode.$Name" -join "`n" - $running = $serviceInfo -match '"PID" = (\d+);' + if ((Test-PodeMacOsServiceIsRegistered $nameService )) { + $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match # Check if the service has a PID entry - if ($running) { - $servicePid = ($running[0].split('= '))[1].trim(';') # Extract the PID from the match - + if ($servicePid -ne 0) { return @{ Name = $Name Status = 'Running' From 87254b33a3366da35ab01cbd761ef1853e770b83 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 16 Nov 2024 19:25:45 -0800 Subject: [PATCH 29/93] add suspend ,resume --- Pode.sln | 2 + examples/HelloService/HelloService.ps1 | 24 +- pode.build.ps1 | 7 +- src/Pode.psd1 | 4 +- src/PodePwshMonitor/IPausableHostedService.cs | 5 + src/PodePwshMonitor/PodeMonitor.csproj | 17 +- src/PodePwshMonitor/PodePwshMain.cs | 125 +++++++++- src/PodePwshMonitor/PodePwshMonitor.cs | 156 ++++++++---- src/PodePwshMonitor/PodePwshWorker.cs | 135 ++++++++-- src/PodePwshMonitor/PodePwshWorkerOptions.cs | 67 ++++- src/PodePwshMonitor/PodeWindowsService.cs | 71 ++++++ src/Private/Helpers.ps1 | 6 +- src/Private/Service.ps1 | 52 ++-- src/Public/Service.ps1 | 232 +++++++++++++++++- 14 files changed, 795 insertions(+), 108 deletions(-) create mode 100644 src/PodePwshMonitor/IPausableHostedService.cs create mode 100644 src/PodePwshMonitor/PodeWindowsService.cs diff --git a/Pode.sln b/Pode.sln index 66eb3805a..a1bd85799 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\PodePwshMonitor\PodeMonitor.csproj", "{A927D6A5-A2AC-471A-9ABA-45916B597EB6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 935239cde..07ddd2612 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -64,7 +64,7 @@ param( [Parameter( ParameterSetName = 'Inbuilt')] [int] $Port = 8080, - + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] [switch] $Register, @@ -87,7 +87,15 @@ param( [Parameter( ParameterSetName = 'Query')] [switch] - $Query + $Query, + + [Parameter( ParameterSetName = 'Suspend')] + [switch] + $Suspend, + + [Parameter( ParameterSetName = 'Resume')] + [switch] + $Resume ) try { # Get the path of the script being executed @@ -112,7 +120,7 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service2' -ParameterString '-Port 8081' + Register-PodeService -Name 'Hello Service2' -ParameterString "-Port $Port" # -Password (ConvertTo-SecureString 'Pata2Pata1' -AsPlainText -Force) exit } if ( $Unregister.IsPresent) { @@ -129,6 +137,16 @@ if ($Stop.IsPresent) { exit } +if ($Suspend.IsPresent) { + Suspend-PodeService -Name 'Hello Service2' + exit +} + +if ($Resume.IsPresent) { + Resume-PodeService -Name 'Hello Service2' + exit +} + if ($Query.IsPresent) { Get-PodeService -Name 'Hello Service2' exit diff --git a/pode.build.ps1 b/pode.build.ps1 index 6eae827d6..d30a6d594 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -113,6 +113,7 @@ function Invoke-PodeBuildDotnetBuild($target) { # Determine if the target framework is compatible $isCompatible = $False switch ($majorVersion) { + 9 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0','net9.0')) { $isCompatible = $True } } 8 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0')) { $isCompatible = $True } } 7 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } 6 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } @@ -165,7 +166,11 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { $AssemblyVersion = '' } foreach ($target in @('win-x64','win-arm64' ,'linux-x64','linux-arm64', 'osx-x64', 'osx-arm64')) { - dotnet publish --configuration Release $AssemblyVersion --runtime $target --output ../Bin/$target + $DefineConstants = '' + if ($target -like 'win-*') { + $DefineConstants = '-p:DefineConstants="WINDOWS"' + } + dotnet publish --runtime $target --output ../Bin/$target --configuration Release $AssemblyVersion "$DefineConstants" if (!$?) { throw "dotnet publish failed for $($target)" } diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 1caa1fdae..8e4ceddde 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -505,7 +505,9 @@ 'Unregister-PodeService', 'Start-PodeService', 'Stop-PodeService', - 'Get-PodeService' + 'Get-PodeService', + 'Suspend-PodeService', + 'Resume-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/PodePwshMonitor/IPausableHostedService.cs b/src/PodePwshMonitor/IPausableHostedService.cs new file mode 100644 index 000000000..4bf9aae1b --- /dev/null +++ b/src/PodePwshMonitor/IPausableHostedService.cs @@ -0,0 +1,5 @@ +public interface IPausableHostedService +{ + void OnPause(); + void OnContinue(); +} \ No newline at end of file diff --git a/src/PodePwshMonitor/PodeMonitor.csproj b/src/PodePwshMonitor/PodeMonitor.csproj index afd537461..67ef61d11 100644 --- a/src/PodePwshMonitor/PodeMonitor.csproj +++ b/src/PodePwshMonitor/PodeMonitor.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 true true true @@ -10,14 +10,13 @@ - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/PodePwshMonitor/PodePwshMain.cs b/src/PodePwshMonitor/PodePwshMain.cs index 6b62a4b1b..bf595b457 100644 --- a/src/PodePwshMonitor/PodePwshMain.cs +++ b/src/PodePwshMonitor/PodePwshMain.cs @@ -1,17 +1,33 @@ 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.Logging; namespace Pode.Services { - public class Program + public static class Program { + private static string logFilePath; public static void Main(string[] args) { var customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Custom config file from args or default + + // Retrieve the service name from the configuration + string serviceName = "PodeService"; + var config = new ConfigurationBuilder() + .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) + .Build(); + serviceName = config.GetSection("PodePwshWorker:Name").Value ?? serviceName; + logFilePath = config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log"; + + + // Configure the host builder var builder = Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { @@ -24,24 +40,127 @@ public static void Main(string[] args) // Add your worker service services.AddHostedService(); + +#if WINDOWS + services.AddSingleton(); // Registers the interface +#endif }); // Check if running on Linux and use Systemd if (OperatingSystem.IsLinux()) { builder.UseSystemd(); + builder.Build().Run(); } // Check if running on Windows and use Windows Service else if (OperatingSystem.IsWindows()) { + //builder.UseWindowsService(); + // Windows-specific logic for CanPauseAndContinue +#if WINDOWS + + + using var host = builder.Build(); + var service = new PodeWindowsService(host,serviceName); + ServiceBase.Run(service); +#else builder.UseWindowsService(); + builder.Build().Run(); +#endif } - else if (OperatingSystem.IsMacOS()) + else if (OperatingSystem.IsMacOS()) { // No specific macOS service manager, it runs under launchd + builder.Build().Run(); + } + else + { + // Fallback for unsupported platforms + Console.WriteLine("Unsupported platform. Exiting."); + return; + } + + } + + public static void Log(string message, params object[] args) + { + if (!string.IsNullOrEmpty(message)) + { + try + { + // Format the message with the provided arguments + var formattedMessage = string.Format(message, args); + + // Write log entry to file, create the file if it doesn't exist + using StreamWriter writer = new(logFilePath, true); + if (formattedMessage.Contains("[Client]")) + { + writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {formattedMessage}"); + } + else + { + writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - [Server] - {formattedMessage}"); + } + + } + catch (Exception ex) + { + Console.WriteLine($"Failed to log to file: {ex.Message}"); + } } + } + + + public static void Log(Exception exception, string message = null, params object[] args) + { + if (exception == null && string.IsNullOrEmpty(message)) + { + return; // Nothing to log + } + + try + { + // Format the message if provided + var logMessage = string.Empty; + + if (!string.IsNullOrEmpty(message)) + { + logMessage = string.Format(message, args); + } + + // Add exception details if provided + if (exception != null) + { + logMessage += $"{Environment.NewLine}Exception: {exception.GetType().Name}"; + logMessage += $"{Environment.NewLine}Message: {exception.Message}"; + logMessage += $"{Environment.NewLine}Stack Trace: {exception.StackTrace}"; - builder.Build().Run(); + // 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; + } + } + + // Write log entry to file + using StreamWriter writer = new(logFilePath, true); + if (logMessage.Contains("[Client]")) + { + writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {logMessage}"); + } + else + { + writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - [Server] - {logMessage}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to log to file: {ex.Message}"); + } } } } diff --git a/src/PodePwshMonitor/PodePwshMonitor.cs b/src/PodePwshMonitor/PodePwshMonitor.cs index 12b3180df..5a7820a3b 100644 --- a/src/PodePwshMonitor/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/PodePwshMonitor.cs @@ -45,7 +45,6 @@ public class PodePwshMonitor private readonly string _scriptPath; private readonly string _parameterString; private string _pwshPath; - private readonly bool _quiet; private readonly bool _disableTermination; private readonly int _shutdownWaitTimeMs; @@ -53,7 +52,7 @@ public class PodePwshMonitor private NamedPipeClientStream _pipeClient; // Changed to client stream private DateTime _lastLogTime; - private readonly string _logFilePath; // Path to log file + // private static string _logFilePath; // Path to log file public PodePwshMonitor(string scriptPath, string pwshPath, string parameterString = "", string logFilePath = ".\\PodePwshMonitorService.log", bool quiet = true, bool disableTermination = true, int shutdownWaitTimeMs = 30000) { @@ -65,7 +64,7 @@ public PodePwshMonitor(string scriptPath, string pwshPath, string parameterStrin _disableTermination = disableTermination; // Flag to disable termination of the service _quiet = quiet; // Flag to suppress output for a quieter service _shutdownWaitTimeMs = shutdownWaitTimeMs; // Maximum wait time before forcefully shutting down the process - _logFilePath = logFilePath ?? ".\\PodePwshMonitorService.log"; // Default to local file if none provided + // _logFilePath = logFilePath ?? ".\\PodePwshMonitorService.log"; // Default to local file if none provided // Dynamically generate a unique PipeName for communication _pipeName = $"PodePipe_{Guid.NewGuid()}"; // Generate a unique pipe name to avoid conflicts @@ -89,18 +88,18 @@ public void StartPowerShellProcess() CreateNoWindow = true // Do not create a new window } }; - Log($"[Server] - Starting ..."); + Program.Log($"Starting ..."); // Properly escape double quotes within the JSON string string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; - Log($"[Server] - Powershell path {_pwshPath}"); - Log($"[Server] - PodeService content:"); - Log($"[Server] - DisableTermination\t= {_disableTermination.ToString().ToLower()}"); - Log($"[Server] - Quiet\t= {_quiet.ToString().ToLower()}"); - Log($"[Server] - PipeName\t= {_pipeName}"); + Program.Log($"Powershell path {_pwshPath}"); + Program.Log($"PodeService content:"); + Program.Log($"DisableTermination\t= {_disableTermination.ToString().ToLower()}"); + Program.Log($"Quiet\t= {_quiet.ToString().ToLower()}"); + Program.Log($"PipeName\t= {_pipeName}"); // Build the PowerShell command with NoProfile and global variable initialization string command = $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; - Log($"[Server] - Starting PowerShell process with command: {command}"); + Program.Log($"Starting PowerShell process with command: {command}"); // Set the arguments for the PowerShell process _powerShellProcess.StartInfo.Arguments = command; @@ -109,17 +108,17 @@ public void StartPowerShellProcess() _powerShellProcess.Start(); // Log output and error asynchronously - _powerShellProcess.OutputDataReceived += (sender, args) => Log(args.Data); - _powerShellProcess.ErrorDataReceived += (sender, args) => Log(args.Data); + _powerShellProcess.OutputDataReceived += (sender, args) => Program.Log(args.Data); + _powerShellProcess.ErrorDataReceived += (sender, args) => Program.Log(args.Data); _powerShellProcess.BeginOutputReadLine(); _powerShellProcess.BeginErrorReadLine(); _lastLogTime = DateTime.Now; - Log("[Server] - PowerShell process started successfully."); + Program.Log("PowerShell process started successfully."); } catch (Exception ex) { - Log($"[Server] - Failed to start PowerShell process: {ex.Message}"); + Program.Log($"Failed to start PowerShell process: {ex.Message}"); } } else @@ -127,7 +126,7 @@ public void StartPowerShellProcess() // Log only if more than a minute has passed since the last log if ((DateTime.Now - _lastLogTime).TotalMinutes >= 1) { - Log("[Server] - PowerShell process is already running."); + Program.Log("PowerShell process is already running."); _lastLogTime = DateTime.Now; } } @@ -138,7 +137,7 @@ public void StopPowerShellProcess() try { _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Log($"[Server] - Connecting to the pipe server using pipe: {_pipeName}"); + Program.Log($"Connecting to the pipe server using pipe: {_pipeName}"); // Connect to the PowerShell pipe server _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection @@ -147,7 +146,7 @@ public void StopPowerShellProcess() { // Send shutdown message and wait for the process to exit SendPipeMessage("shutdown"); - Log($"[Server] - Waiting up to {_shutdownWaitTimeMs} milliseconds for the PowerShell process to exit..."); + Program.Log($"Waiting up to {_shutdownWaitTimeMs} milliseconds for the PowerShell process to exit..."); // Timeout logic int waited = 0; @@ -161,16 +160,16 @@ public void StopPowerShellProcess() if (_powerShellProcess.HasExited) { - Log("[Server] - PowerShell process has been shutdown gracefully."); + Program.Log("PowerShell process has been shutdown gracefully."); } else { - Log($"[Server] - PowerShell process did not exit in {_shutdownWaitTimeMs} milliseconds."); + Program.Log($"PowerShell process did not exit in {_shutdownWaitTimeMs} milliseconds."); } } else { - Log($"[Server] - Failed to connect to the PowerShell pipe server using pipe: {_pipeName}"); + Program.Log($"Failed to connect to the PowerShell pipe server using pipe: {_pipeName}"); } // Forcefully kill the process if it's still running @@ -179,17 +178,17 @@ public void StopPowerShellProcess() try { _powerShellProcess.Kill(); - Log("[Server] - PowerShell process killed successfully."); + Program.Log("PowerShell process killed successfully."); } catch (Exception ex) { - Log($"[Server] - Error killing PowerShell process: {ex.Message}"); + Program.Log($"Error killing PowerShell process: {ex.Message}"); } } } catch (Exception ex) { - Log($"[Server] - Error stopping PowerShell process: {ex.Message}"); + Program.Log($"Error stopping PowerShell process: {ex.Message}"); } finally { @@ -205,8 +204,8 @@ public void StopPowerShellProcess() _pipeClient?.Dispose(); _pipeClient = null; } - Log("[Server] - PowerShell process and pipe client disposed."); - Log("[Server] - Done."); + Program.Log("PowerShell process and pipe client disposed."); + Program.Log("Done."); } } @@ -216,57 +215,108 @@ public void RestartPowerShellProcess() if (_pipeClient != null && _pipeClient.IsConnected) { SendPipeMessage("restart"); // Inform PowerShell about the restart - Log("[Server] - Restart message sent to PowerShell."); + Program.Log("Restart message sent to PowerShell."); + } + } + + public void SuspendPowerShellProcess() + { + try + { + _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); + Program.Log($"Connecting to the pipe server using pipe: {_pipeName}"); + + // Connect to the PowerShell pipe server + _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection + // Simply send the restart message, no need to stop and start again + if (_pipeClient != null && _pipeClient.IsConnected) + { + Program.Log("maybe I'm here too."); + SendPipeMessage("suspend"); // Inform PowerShell about the restart + Program.Log("Suspend message sent to PowerShell."); + } + } + catch (Exception ex) + { + Program.Log(ex, $"Error suspending PowerShell process"); + } + finally + { + // Clean up the pipe client + if (_pipeClient != null) + { + _pipeClient?.Dispose(); + _pipeClient = null; + } + Program.Log("PowerShell process and pipe client disposed."); + Program.Log("Done."); + } + } + + public void ResumePowerShellProcess() + { + try + { + _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); + Program.Log($"Connecting to the pipe server using pipe: {_pipeName}"); + + // Connect to the PowerShell pipe server + _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection + // Simply send the restart message, no need to stop and start again + if (_pipeClient != null && _pipeClient.IsConnected) + { + SendPipeMessage("resume"); // Inform PowerShell about the restart + Program.Log("Resume message sent to PowerShell."); + } + } + catch (Exception ex) + { + Program.Log(ex, $"Error resuming PowerShell process"); + } + finally + { + // Clean up the pipe client + if (_pipeClient != null) + { + _pipeClient?.Dispose(); + _pipeClient = null; + } + Program.Log("PowerShell process and pipe client disposed."); + Program.Log("Done."); } } private void SendPipeMessage(string message) { + Program.Log("SendPipeMessage: {0}", message); + + // Write the message to the pipe + if (_pipeClient == null) { - Log("[Server] - Pipe client is not initialized, cannot send message."); + Program.Log("Pipe client is not initialized, cannot send message."); return; } if (!_pipeClient.IsConnected) { - Log("[Server] - Pipe client is not connected, cannot send message."); + Program.Log("Pipe client is not connected, cannot send message."); return; } try { // Send the message using the pipe client stream - using (var writer = new StreamWriter(_pipeClient, leaveOpen: true)) // leaveOpen to keep the pipe alive for multiple writes - { - writer.AutoFlush = true; - writer.WriteLine(message); - Log($"[Server] - Message sent to PowerShell: {message}"); - } + using var writer = new StreamWriter(_pipeClient, leaveOpen: true); // leaveOpen to keep the pipe alive for multiple writes + writer.AutoFlush = true; + writer.WriteLine(message); + Program.Log($"Message sent to PowerShell: {message}"); } catch (Exception ex) { - Log($"[Server] - Failed to send message to PowerShell: {ex.Message}"); + Program.Log($"Failed to send message to PowerShell: {ex.Message}"); } } - private void Log(string data) - { - if (!string.IsNullOrEmpty(data)) - { - try - { - // Write log entry to file, create the file if it doesn't exist - using (StreamWriter writer = new StreamWriter(_logFilePath, true)) - { - writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {data}"); - } - } - catch (Exception ex) - { - Console.WriteLine($"[Server] - Failed to log to file: {ex.Message}"); - } - } - } } } diff --git a/src/PodePwshMonitor/PodePwshWorker.cs b/src/PodePwshMonitor/PodePwshWorker.cs index 393de4791..3d316d176 100644 --- a/src/PodePwshMonitor/PodePwshWorker.cs +++ b/src/PodePwshMonitor/PodePwshWorker.cs @@ -8,10 +8,14 @@ namespace Pode.Services { - public class PodePwshWorker : BackgroundService + public sealed class PodePwshWorker : BackgroundService, IPausableHostedService { private readonly ILogger _logger; private PodePwshMonitor _pwshMonitor; + private readonly int maxRetryCount; // Maximum number of retries before breaking + private readonly int retryDelayMs; // Delay between retries in milliseconds + + private volatile bool _isPaused; public PodePwshWorker(ILogger logger, IOptions options) { @@ -20,8 +24,8 @@ public PodePwshWorker(ILogger logger, IOptions logger, IOptions= maxRetryCount) + { + _logger.LogCritical("Maximum retry count reached. Breaking the monitoring loop."); + break; // Exit the loop + } + + // Wait for a while before retrying + try + { + await Task.Delay(retryDelayMs, stoppingToken); + } + catch (OperationCanceledException) + { + Program.Log("Operation canceled during retry delay."); + break; // Exit the loop if the operation is canceled + } + } + // Wait before the next monitoring iteration + await Task.Delay(10000, stoppingToken); } + + Program.Log("Monitoring loop has stopped."); } - public override Task StopAsync(CancellationToken stoppingToken) + + public override async Task StopAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Service is stopping at: {time}", DateTimeOffset.Now); - _pwshMonitor.StopPowerShellProcess(); - return base.StopAsync(stoppingToken); + Program.Log("Service is stopping at: {0}", DateTimeOffset.Now); + try + { + _pwshMonitor.StopPowerShellProcess(); + } + catch (Exception ex) + { + Program.Log(ex, "Error while stopping PowerShell process: {message}", ex.Message); + } + // Wait for the base StopAsync to complete + await base.StopAsync(stoppingToken); + + Program.Log("Service stopped successfully at: {0}", DateTimeOffset.Now); + } // Custom RestartAsync method that sends a restart message via pipe - public Task RestartAsync() + public void RestartAsync() + { + Program.Log("Service is restarting at: {0}", DateTimeOffset.Now); + try + { + // Send the 'restart' message using the pipe + _pwshMonitor.RestartPowerShellProcess(); + + Program.Log("Restart message sent via pipe at: {0}", DateTimeOffset.Now); + + } + catch (Exception ex) + { + Program.Log(ex, "An error occurred during restart: {message}", ex.Message); + } + } + + + public void OnPause() { - _logger.LogInformation("Service is restarting at: {time}", DateTimeOffset.Now); + Program.Log("Pause command received at: {0}", DateTimeOffset.Now); - // Send the 'restart' message using the pipe - _pwshMonitor.RestartPowerShellProcess(); + try + { + _pwshMonitor.SuspendPowerShellProcess(); + _isPaused = true; + Program.Log("Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + } + catch (Exception ex) + { + Program.Log(ex, "Error occurred while suspending PowerShell process: {message}", ex.Message); + } + } + + public void OnContinue() + { + Program.Log("Continue command received at: {0}", DateTimeOffset.Now); - _logger.LogInformation("Restart message sent via pipe at: {time}", DateTimeOffset.Now); - return Task.CompletedTask; + try + { + _pwshMonitor.ResumePowerShellProcess(); + _isPaused = false; + Program.Log("Resume message sent via pipe at: {0}", DateTimeOffset.Now); + } + catch (Exception ex) + { + Program.Log(ex, "Error occurred while resuming PowerShell process: {message}", ex.Message); + } } } } diff --git a/src/PodePwshMonitor/PodePwshWorkerOptions.cs b/src/PodePwshMonitor/PodePwshWorkerOptions.cs index 7424db84b..e2efb2160 100644 --- a/src/PodePwshMonitor/PodePwshWorkerOptions.cs +++ b/src/PodePwshMonitor/PodePwshWorkerOptions.cs @@ -1,20 +1,81 @@ +using System; + namespace Pode.Services { + /// + /// Configuration options for the PodePwshWorker service. + /// These options determine how the worker operates, including paths, parameters, and retry policies. + /// public class PodePwshWorkerOptions { + + /// + /// 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; } = ""; + + /// + /// 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; + + /// + /// 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 $"ScriptPath: {ScriptPath}, PwshPath: {PwshPath}, ParameterString: {ParameterString}, " + - $"LogFilePath: {LogFilePath}, Quiet: {Quiet}, DisableTermination: {DisableTermination}, " + - $"ShutdownWaitTimeMs: {ShutdownWaitTimeMs}"; + return $"Name: {Name}, ScriptPath: {ScriptPath}, PwshPath: {PwshPath}, ParameterString: {ParameterString}, " + + $"LogFilePath: {LogFilePath}, Quiet: {Quiet}, DisableTermination: {DisableTermination}, " + + $"ShutdownWaitTimeMs: {ShutdownWaitTimeMs}, StartMaxRetryCount: {StartMaxRetryCount}, " + + $"StartRetryDelayMs: {StartRetryDelayMs}"; } } } diff --git a/src/PodePwshMonitor/PodeWindowsService.cs b/src/PodePwshMonitor/PodeWindowsService.cs new file mode 100644 index 000000000..fa0eec836 --- /dev/null +++ b/src/PodePwshMonitor/PodeWindowsService.cs @@ -0,0 +1,71 @@ +#if WINDOWS +using System; +using Microsoft.Extensions.Hosting; +using System.ServiceProcess; +using System.Runtime.Versioning; + +namespace Pode.Services +{ + [SupportedOSPlatform("windows")] + public class PodeWindowsService : ServiceBase + { + private readonly IHost _host; + + + public PodeWindowsService(IHost host, string serviceName ) + { + _host = host; + CanPauseAndContinue = true; + ServiceName = serviceName; // Dynamically set the service name + } + + protected override void OnStart(string[] args) + { + Program.Log("Service starting..."); + base.OnStart(args); + _host.StartAsync().Wait(); + Program.Log("Service started successfully."); + } + + protected override void OnStop() + { + Program.Log("Service stopping..."); + base.OnStop(); + _host.StopAsync().Wait(); + Program.Log("Service stopped successfully."); + } + + protected override void OnPause() + { + Program.Log("Service pausing..."); + base.OnPause(); + var service = _host.Services.GetService(typeof(IPausableHostedService)); + if (service != null) + { + Program.Log($"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnPause(); + } + else + { + Program.Log("Error:Failed to resolve IPausableHostedService."); + } + } + + protected override void OnContinue() + { + Program.Log("Service resuming..."); + base.OnContinue(); + var service = _host.Services.GetService(typeof(IPausableHostedService)); + if (service != null) + { + Program.Log($"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnContinue(); + } + else + { + Program.Log("Error:Failed to resolve IPausableHostedService."); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 07863fa1c..fc16ff38f 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3885,13 +3885,17 @@ function Invoke-PodeWinElevatedCommand { [string] $Command, [string] - $Arguments + $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 '"', '"""' diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index b446e2082..d01d8de5b 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -88,20 +88,38 @@ function Start-PodeServiceHearthbeat { if ($message) { Write-PodeHost -Message "[Client] - Received message: $message" -Force - # Process 'shutdown' message - if ($message -eq 'shutdown') { - - Write-PodeHost -Message '[Client] - Server requested shutdown. Closing client...' -Force - Close-PodeServer # Gracefully stop the Pode server - return # Exit the loop - - # Process 'restart' message - } - elseif ($message -eq 'restart') { - Write-PodeHost -Message '[Client] - Server requested restart. Restarting client...' -Force - Restart-PodeServer # Restart the Pode server - return # Exit the loop + switch ($message) { + 'shutdown' { + # Process 'shutdown' message + Write-PodeHost -Message '[Client] - Server requested shutdown. Closing client...' -Force + Close-PodeServer # Gracefully stop Pode server + return # Exit the loop + } + + 'restart' { + # Process 'restart' message + Write-PodeHost -Message '[Client] - Server requested restart. Restarting client...' -Force + Restart-PodeServer # Restart Pode server + return # Exit the loop + } + + 'suspend' { + # Process 'suspend' message + Write-PodeHost -Message '[Client] - Server requested suspend. Suspending client...' -Force + Start-Sleep 5 + #Suspend-PodeServer # Suspend Pode server + return # Exit the loop + } + + 'resume' { + # Process 'resume' message + Write-PodeHost -Message '[Client] - Server requested resume. Resuming client...' -Force + Start-Sleep 5 + #Resume-PodeServer # Resume Pode server + return # Exit the loop + } } + } } } @@ -521,9 +539,6 @@ function Register-PodeWindowsService { Description = $Description #DependsOn = 'NetLogon' } - if ($Credential) { - $params['Credential'] = $Credential - } if ($SecurityDescriptorSddl) { $params['SecurityDescriptorSddl'] = $SecurityDescriptorSddl } @@ -531,7 +546,10 @@ function Register-PodeWindowsService { try { $paramsString = $params.GetEnumerator() | ForEach-Object { "-$($_.Key) '$($_.Value)'" } - $sv = Invoke-PodeWinElevatedCommand -Command 'New-Service' -Arguments ($paramsString -join ' ') + + $sv = Invoke-PodeWinElevatedCommand -Command 'New-Service' -Arguments ($paramsString -join ' ') -Credential $Credential + + if (!$sv) { # Service registration failed. diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index e564e702e..2a0818989 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -412,7 +412,7 @@ function Stop-PodeService { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running - if ($service.Status -eq 'Running') { + if ($service.Status -eq 'Running' -or $service.Status -eq 'Paused') { $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Stopped') { @@ -496,6 +496,236 @@ function Stop-PodeService { } +<# +.SYNOPSIS + Stops a Pode-based service across different platforms (Windows, Linux, and macOS). + +.DESCRIPTION + The `Stop-PodeService` function stops a Pode-based service by checking if it is currently running. + If the service is running, it will attempt to stop the service gracefully. + The function works on Windows, Linux (systemd), and macOS (launchctl). + +.PARAMETER Name + The name of the service. + +.EXAMPLE + Stop-PodeService + + Stops the Pode-based service if it is currently running. If the service is not running, no action is taken. + +.NOTES + - The function retrieves the service name from the `srvsettings.json` file located in the script directory. + - On Windows, it uses `Get-Service` and `Stop-Service`. + - On Linux, it uses `systemctl` to stop the service. + - On macOS, it uses `launchctl` to stop the service. + - If the service is not registered, the function throws an error. +#> +function Suspend-PodeService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + try { + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege + + if ($IsWindows) { + + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is running + if ($service.Status -eq 'Running') { + $null = Invoke-PodeWinElevatedCommand -Command 'Suspend-Service' -Arguments "-Name '$Name'" + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service.Status -eq 'Paused') { + Write-Verbose -Message "Service '$Name' suspended successfully." + } + else { + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'Suspend-Service', $Name) + } + } + else { + Write-Verbose -Message "Service '$Name' is not running." + return $false + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + } + elseif ($IsLinux) { + <# $nameService = "$Name.service".Replace(' ', '_') + # Check if the service is already registered + if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { + # Check if the service is active + if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { + #Stop the service + if (( Stop-PodeLinuxService -Name $nameService)) { + # Check if the service is active + if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + return $true + } + } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo launchctl stop', $Name) + } + else { + Write-Verbose -Message "Service '$Name' is not running." + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) + } +#> + } + elseif ($IsMacOS) { + <# + $nameService = "pode.$Name.service".Replace(' ', '_') + # Check if the service is already registered + if ((Test-PodeMacOsServiceIsRegistered -Name $nameService)) { + # Check if the service is active + if ((Test-PodeMacOsServiceIsActive $nameService)) { + if ((Stop-PodeMacOsService $nameService)) { + if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + return $true + } + } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) + + } + else { + Write-Verbose -Message "Service '$Name' is not running." + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) + } + #> + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } + return $true +} + + + +function Resume-PodeService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + try { + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege + + if ($IsWindows) { + + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is running + if ($service.Status -eq 'Paused') { + $null = Invoke-PodeWinElevatedCommand -Command 'Resume-Service' -Arguments "-Name '$Name'" + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service.Status -eq 'Running') { + Write-Verbose -Message "Service '$Name' Resumed successfully." + } + else { + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'Resume-Service', $Name) + } + } + else { + Write-Verbose -Message "Service '$Name' is not suspended." + return $false + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + } + elseif ($IsLinux) { + <# $nameService = "$Name.service".Replace(' ', '_') + # Check if the service is already registered + if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { + # Check if the service is active + if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { + #Stop the service + if (( Stop-PodeLinuxService -Name $nameService)) { + # Check if the service is active + if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + return $true + } + } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo launchctl stop', $Name) + } + else { + Write-Verbose -Message "Service '$Name' is not running." + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) + } +#> + } + elseif ($IsMacOS) { + <# + $nameService = "pode.$Name.service".Replace(' ', '_') + # Check if the service is already registered + if ((Test-PodeMacOsServiceIsRegistered -Name $nameService)) { + # Check if the service is active + if ((Test-PodeMacOsServiceIsActive $nameService)) { + if ((Stop-PodeMacOsService $nameService)) { + if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + return $true + } + } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) + + } + else { + Write-Verbose -Message "Service '$Name' is not running." + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) + } + #> + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $false + } + return $true +} + <# .SYNOPSIS Unregisters a Pode-based service across different platforms (Windows, Linux, and macOS). From ebf36a2a739660a57e74372f1b59b781159aa27d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 17 Nov 2024 09:56:45 -0800 Subject: [PATCH 30/93] Improvements --- src/Locales/ar/Pode.psd1 | 1 + src/Locales/de/Pode.psd1 | 1 + src/Locales/en-us/Pode.psd1 | 1 + src/Locales/en/Pode.psd1 | 1 + src/Locales/es/Pode.psd1 | 1 + src/Locales/fr/Pode.psd1 | 1 + src/Locales/it/Pode.psd1 | 1 + src/Locales/ja/Pode.psd1 | 1 + src/Locales/ko/Pode.psd1 | 1 + src/Locales/nl/Pode.psd1 | 1 + src/Locales/pl/Pode.psd1 | 1 + src/Locales/pt/Pode.psd1 | 1 + src/Locales/zh/Pode.psd1 | 1 + src/PodePwshMonitor/IPausableHostedService.cs | 5 - src/PodePwshMonitor/PodeMonitor.csproj | 1 - src/PodePwshMonitor/PodePwshMain.cs | 166 ----------------- src/PodePwshMonitor/PodePwshWorker.cs | 170 ------------------ src/PodePwshMonitor/PodeWindowsService.cs | 71 -------- .../Service/IPausableHostedService.cs | 8 + src/PodePwshMonitor/Service/Logger.cs | 155 ++++++++++++++++ src/PodePwshMonitor/Service/PodePwshMain.cs | 109 +++++++++++ .../{ => Service}/PodePwshMonitor.cs | 138 +++++++------- src/PodePwshMonitor/Service/PodePwshWorker.cs | 159 ++++++++++++++++ .../{ => Service}/PodePwshWorkerOptions.cs | 2 +- .../Service/PodeWindowsService.cs | 86 +++++++++ src/Private/Service.ps1 | 7 +- src/Public/Service.ps1 | 168 +++++------------ 27 files changed, 653 insertions(+), 605 deletions(-) delete mode 100644 src/PodePwshMonitor/IPausableHostedService.cs delete mode 100644 src/PodePwshMonitor/PodePwshMain.cs delete mode 100644 src/PodePwshMonitor/PodePwshWorker.cs delete mode 100644 src/PodePwshMonitor/PodeWindowsService.cs create mode 100644 src/PodePwshMonitor/Service/IPausableHostedService.cs create mode 100644 src/PodePwshMonitor/Service/Logger.cs create mode 100644 src/PodePwshMonitor/Service/PodePwshMain.cs rename src/PodePwshMonitor/{ => Service}/PodePwshMonitor.cs (58%) create mode 100644 src/PodePwshMonitor/Service/PodePwshWorker.cs rename src/PodePwshMonitor/{ => Service}/PodePwshWorkerOptions.cs (99%) create mode 100644 src/PodePwshMonitor/Service/PodeWindowsService.cs diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index acbc7f432..b90907e9e 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -299,4 +299,5 @@ serviceIsRunningException = "الخدمة '{0}' تعمل. استخدم المعامل -Force للإيقاف بالقوة." serviceUnRegistrationException = "فشل إلغاء تسجيل الخدمة '{0}'." passwordRequiredForServiceUserException = "مطلوب كلمة مرور عند تحديد مستخدم الخدمة في نظام Windows. يرجى تقديم كلمة مرور صالحة للمستخدم '{0}'." + featureNotSupportedException = '{0} مدعومة فقط على نظام التشغيل Windows.' } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index 939789f76..72b872acc 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 473a671fa..b3078c12f 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index cc5398fdb..4740f95fc 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 5bb44e0b0..6601c1bcb 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 5af56c4ba..a4cc543d5 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 00b44b742..e2976a630 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 2133ea10a..ce4be3394 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -299,4 +299,5 @@ serviceIsRunningException = "サービス '{0}' が実行中です。強制的に停止するには、-Force パラメーターを使用してください。" serviceUnRegistrationException = "サービス '{0}' の登録解除に失敗しました。" passwordRequiredForServiceUserException = "Windowsでサービスユーザーを指定する際にはパスワードが必要です。ユーザー '{0}' に有効なパスワードを入力してください。" + featureNotSupportedException = '{0} は Windows のみでサポートされています。' } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 537c198fe..7eba12611 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -299,4 +299,5 @@ serviceIsRunningException = "서비스 '{0}'가 실행 중입니다. 강제로 중지하려면 -Force 매개변수를 사용하세요." serviceUnRegistrationException = "서비스 '{0}' 등록 취소에 실패했습니다." passwordRequiredForServiceUserException = "Windows에서 서비스 사용자를 지정할 때는 비밀번호가 필요합니다. 사용자 '{0}'에 대해 유효한 비밀번호를 입력하세요." + featureNotSupportedException = '{0}는 Windows에서만 지원됩니다.' } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index 9a9d9f37f..74bb1da94 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index b7e7913ba..f9be3abed 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index feb0065d0..05d6026fc 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -299,4 +299,5 @@ 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.' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 477e13c54..3abb39319 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -299,4 +299,5 @@ serviceIsRunningException = "服务 '{0}' 正在运行。使用 -Force 参数强制停止。" serviceUnRegistrationException = "服务 '{0}' 的注销失败。" passwordRequiredForServiceUserException = "在 Windows 中指定服务用户时需要密码。请为用户 '{0}' 提供有效的密码。" + featureNotSupportedException = '{0} 仅支持 Windows。' } \ No newline at end of file diff --git a/src/PodePwshMonitor/IPausableHostedService.cs b/src/PodePwshMonitor/IPausableHostedService.cs deleted file mode 100644 index 4bf9aae1b..000000000 --- a/src/PodePwshMonitor/IPausableHostedService.cs +++ /dev/null @@ -1,5 +0,0 @@ -public interface IPausableHostedService -{ - void OnPause(); - void OnContinue(); -} \ No newline at end of file diff --git a/src/PodePwshMonitor/PodeMonitor.csproj b/src/PodePwshMonitor/PodeMonitor.csproj index 67ef61d11..abbe53c35 100644 --- a/src/PodePwshMonitor/PodeMonitor.csproj +++ b/src/PodePwshMonitor/PodeMonitor.csproj @@ -16,7 +16,6 @@ - \ No newline at end of file diff --git a/src/PodePwshMonitor/PodePwshMain.cs b/src/PodePwshMonitor/PodePwshMain.cs deleted file mode 100644 index bf595b457..000000000 --- a/src/PodePwshMonitor/PodePwshMain.cs +++ /dev/null @@ -1,166 +0,0 @@ -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.Logging; - - -namespace Pode.Services -{ - public static class Program - { - private static string logFilePath; - public static void Main(string[] args) - { - var customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Custom config file from args or default - - - // Retrieve the service name from the configuration - string serviceName = "PodeService"; - var config = new ConfigurationBuilder() - .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) - .Build(); - serviceName = config.GetSection("PodePwshWorker:Name").Value ?? serviceName; - logFilePath = config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log"; - - - // Configure the host builder - var builder = Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile(customConfigFile, optional: false, reloadOnChange: true); - }) - .ConfigureServices((context, services) => - { - // Bind configuration to PodePwshWorkerOptions - services.Configure(context.Configuration.GetSection("PodePwshWorker")); - - // Add your worker service - services.AddHostedService(); - -#if WINDOWS - services.AddSingleton(); // Registers the interface -#endif - }); - - // Check if running on Linux and use Systemd - if (OperatingSystem.IsLinux()) - { - builder.UseSystemd(); - builder.Build().Run(); - } - // Check if running on Windows and use Windows Service - else if (OperatingSystem.IsWindows()) - { - //builder.UseWindowsService(); - // Windows-specific logic for CanPauseAndContinue -#if WINDOWS - - - using var host = builder.Build(); - var service = new PodeWindowsService(host,serviceName); - ServiceBase.Run(service); -#else - builder.UseWindowsService(); - builder.Build().Run(); -#endif - } - else if (OperatingSystem.IsMacOS()) - { - // No specific macOS service manager, it runs under launchd - builder.Build().Run(); - } - else - { - // Fallback for unsupported platforms - Console.WriteLine("Unsupported platform. Exiting."); - return; - } - - } - - public static void Log(string message, params object[] args) - { - if (!string.IsNullOrEmpty(message)) - { - try - { - // Format the message with the provided arguments - var formattedMessage = string.Format(message, args); - - // Write log entry to file, create the file if it doesn't exist - using StreamWriter writer = new(logFilePath, true); - if (formattedMessage.Contains("[Client]")) - { - writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {formattedMessage}"); - } - else - { - writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - [Server] - {formattedMessage}"); - } - - } - catch (Exception ex) - { - Console.WriteLine($"Failed to log to file: {ex.Message}"); - } - } - } - - - public static void Log(Exception exception, string message = null, params object[] args) - { - if (exception == null && string.IsNullOrEmpty(message)) - { - return; // Nothing to log - } - - try - { - // Format the message if provided - var logMessage = string.Empty; - - if (!string.IsNullOrEmpty(message)) - { - logMessage = string.Format(message, args); - } - - // Add exception details if provided - 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; - } - } - - // Write log entry to file - using StreamWriter writer = new(logFilePath, true); - if (logMessage.Contains("[Client]")) - { - writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {logMessage}"); - } - else - { - writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - [Server] - {logMessage}"); - } - } - catch (Exception ex) - { - Console.WriteLine($"Failed to log to file: {ex.Message}"); - } - } - } -} diff --git a/src/PodePwshMonitor/PodePwshWorker.cs b/src/PodePwshMonitor/PodePwshWorker.cs deleted file mode 100644 index 3d316d176..000000000 --- a/src/PodePwshMonitor/PodePwshWorker.cs +++ /dev/null @@ -1,170 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Pode.Services -{ - public sealed class PodePwshWorker : BackgroundService, IPausableHostedService - { - private readonly ILogger _logger; - private PodePwshMonitor _pwshMonitor; - private readonly int maxRetryCount; // Maximum number of retries before breaking - private readonly int retryDelayMs; // Delay between retries in milliseconds - - private volatile bool _isPaused; - - public PodePwshWorker(ILogger logger, IOptions options) - { - _logger = logger; - - // Get options from configuration - var workerOptions = options.Value; - // Print options to console - Program.Log("Worker options: "); - Program.Log(workerOptions.ToString()); - - _pwshMonitor = new PodePwshMonitor( - workerOptions.ScriptPath, - workerOptions.PwshPath, - workerOptions.ParameterString, - workerOptions.LogFilePath, - workerOptions.Quiet, - workerOptions.DisableTermination, - workerOptions.ShutdownWaitTimeMs - ); - - maxRetryCount = workerOptions.StartMaxRetryCount; - retryDelayMs = workerOptions.StartRetryDelayMs; - - Program.Log("PodePwshWorker initialized with options: {@Options}", workerOptions); - - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - Program.Log("PodePwshWorker running at: {0}", DateTimeOffset.Now); - - int retryCount = 0; - - while (!stoppingToken.IsCancellationRequested) - { - if (_isPaused) - { - Program.Log("Worker is paused. Waiting..."); - await Task.Delay(1000, stoppingToken); // Pause handling - continue; - } - try - { - // Reset retry count on successful execution - retryCount = 0; - - // Start the PowerShell process - _pwshMonitor.StartPowerShellProcess(); - - } - catch (Exception ex) - { - retryCount++; - Program.Log(ex, "An error occurred in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, maxRetryCount); - - // Check if maximum retries have been reached - if (retryCount >= maxRetryCount) - { - _logger.LogCritical("Maximum retry count reached. Breaking the monitoring loop."); - break; // Exit the loop - } - - // Wait for a while before retrying - try - { - await Task.Delay(retryDelayMs, stoppingToken); - } - catch (OperationCanceledException) - { - Program.Log("Operation canceled during retry delay."); - break; // Exit the loop if the operation is canceled - } - } - // Wait before the next monitoring iteration - await Task.Delay(10000, stoppingToken); - } - - Program.Log("Monitoring loop has stopped."); - } - - - public override async Task StopAsync(CancellationToken stoppingToken) - { - Program.Log("Service is stopping at: {0}", DateTimeOffset.Now); - try - { - _pwshMonitor.StopPowerShellProcess(); - } - catch (Exception ex) - { - Program.Log(ex, "Error while stopping PowerShell process: {message}", ex.Message); - } - // Wait for the base StopAsync to complete - await base.StopAsync(stoppingToken); - - Program.Log("Service stopped successfully at: {0}", DateTimeOffset.Now); - - } - - // Custom RestartAsync method that sends a restart message via pipe - public void RestartAsync() - { - Program.Log("Service is restarting at: {0}", DateTimeOffset.Now); - try - { - // Send the 'restart' message using the pipe - _pwshMonitor.RestartPowerShellProcess(); - - Program.Log("Restart message sent via pipe at: {0}", DateTimeOffset.Now); - - } - catch (Exception ex) - { - Program.Log(ex, "An error occurred during restart: {message}", ex.Message); - } - } - - - public void OnPause() - { - Program.Log("Pause command received at: {0}", DateTimeOffset.Now); - - try - { - _pwshMonitor.SuspendPowerShellProcess(); - _isPaused = true; - Program.Log("Suspend message sent via pipe at: {0}", DateTimeOffset.Now); - } - catch (Exception ex) - { - Program.Log(ex, "Error occurred while suspending PowerShell process: {message}", ex.Message); - } - } - - public void OnContinue() - { - Program.Log("Continue command received at: {0}", DateTimeOffset.Now); - - try - { - _pwshMonitor.ResumePowerShellProcess(); - _isPaused = false; - Program.Log("Resume message sent via pipe at: {0}", DateTimeOffset.Now); - } - catch (Exception ex) - { - Program.Log(ex, "Error occurred while resuming PowerShell process: {message}", ex.Message); - } - } - } -} diff --git a/src/PodePwshMonitor/PodeWindowsService.cs b/src/PodePwshMonitor/PodeWindowsService.cs deleted file mode 100644 index fa0eec836..000000000 --- a/src/PodePwshMonitor/PodeWindowsService.cs +++ /dev/null @@ -1,71 +0,0 @@ -#if WINDOWS -using System; -using Microsoft.Extensions.Hosting; -using System.ServiceProcess; -using System.Runtime.Versioning; - -namespace Pode.Services -{ - [SupportedOSPlatform("windows")] - public class PodeWindowsService : ServiceBase - { - private readonly IHost _host; - - - public PodeWindowsService(IHost host, string serviceName ) - { - _host = host; - CanPauseAndContinue = true; - ServiceName = serviceName; // Dynamically set the service name - } - - protected override void OnStart(string[] args) - { - Program.Log("Service starting..."); - base.OnStart(args); - _host.StartAsync().Wait(); - Program.Log("Service started successfully."); - } - - protected override void OnStop() - { - Program.Log("Service stopping..."); - base.OnStop(); - _host.StopAsync().Wait(); - Program.Log("Service stopped successfully."); - } - - protected override void OnPause() - { - Program.Log("Service pausing..."); - base.OnPause(); - var service = _host.Services.GetService(typeof(IPausableHostedService)); - if (service != null) - { - Program.Log($"Resolved IPausableHostedService: {service.GetType().FullName}"); - ((IPausableHostedService)service).OnPause(); - } - else - { - Program.Log("Error:Failed to resolve IPausableHostedService."); - } - } - - protected override void OnContinue() - { - Program.Log("Service resuming..."); - base.OnContinue(); - var service = _host.Services.GetService(typeof(IPausableHostedService)); - if (service != null) - { - Program.Log($"Resolved IPausableHostedService: {service.GetType().FullName}"); - ((IPausableHostedService)service).OnContinue(); - } - else - { - Program.Log("Error:Failed to resolve IPausableHostedService."); - } - } - } -} -#endif \ No newline at end of file diff --git a/src/PodePwshMonitor/Service/IPausableHostedService.cs b/src/PodePwshMonitor/Service/IPausableHostedService.cs new file mode 100644 index 000000000..bf11b0c6c --- /dev/null +++ b/src/PodePwshMonitor/Service/IPausableHostedService.cs @@ -0,0 +1,8 @@ +namespace Pode.Service +{ + public interface IPausableHostedService + { + void OnPause(); + void OnContinue(); + } +} \ No newline at end of file diff --git a/src/PodePwshMonitor/Service/Logger.cs b/src/PodePwshMonitor/Service/Logger.cs new file mode 100644 index 000000000..33704e6f9 --- /dev/null +++ b/src/PodePwshMonitor/Service/Logger.cs @@ -0,0 +1,155 @@ + +using System; +using System.IO; + +namespace Pode.Service +{ + using System; + using System.IO; + + public enum LogLevel + { + 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 + } + + public static class Logger + { + private static readonly object _logLock = new(); + private static string logFilePath = "PodeService.log"; // Default log file path + private static LogLevel minLogLevel = LogLevel.INFO; // Default minimum log level + + /// + /// Initializes the logger with a custom log file path and minimum log level. + /// + /// Path to the log file. + /// Minimum log level to record. + public static void Initialize(string filePath, LogLevel level) + { + if (!string.IsNullOrWhiteSpace(filePath)) + { + logFilePath = filePath; + } + + minLogLevel = level; + + try + { + // Create the log file if it doesn't exist + if (!File.Exists(logFilePath)) + { + using (File.Create(logFilePath)) { } + } + + Log(LogLevel.INFO, "Server", "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", logFilePath, minLogLevel); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to initialize logger: {ex.Message}"); + } + } + + public static void Log(LogLevel level, string context = "Client", string message = "", params object[] args) + { + if (level < minLogLevel || string.IsNullOrEmpty(message)) + { + return; // Skip logging for levels below the minimum log level + } + + try + { + // Format the message with the provided arguments + var formattedMessage = string.Format(message, args); + + // Ensure context is not null or empty + context = string.IsNullOrWhiteSpace(context) ? "Client" : context; + + // Get the current time in ISO 8601 format in GMT/UTC + string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + + // Get the current process ID + int pid = Environment.ProcessId; + + // Build the log entry + string logEntry = $"{timestamp} [PID:{pid}] [{level}] [{context}] {formattedMessage}"; + + // Thread-safe write to log file + lock (_logLock) + { + using StreamWriter writer = new(logFilePath, true); + writer.WriteLine(logEntry); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to log to file: {ex.Message}"); + } + } + + + public static void Log(LogLevel level, Exception exception, string message = null, params object[] args) + { + if (level < minLogLevel) + { + return; // Skip logging for levels below the minimum log level + } + + if (exception == null && string.IsNullOrEmpty(message)) + { + return; // Nothing to log + } + + try + { + // Get the current time in ISO 8601 format in GMT/UTC + string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + + // Format the message if provided + var logMessage = string.Empty; + + if (!string.IsNullOrEmpty(message)) + { + logMessage = string.Format(message, 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; + } + } + + // Get the current process ID + int pid = Environment.ProcessId; + + // Build the log entry + string logEntry = $"{timestamp} [PID:{pid}] [{level}] [Server] {logMessage}"; + + // Thread-safe write to log file + lock (_logLock) + { + using StreamWriter writer = new(logFilePath, true); + writer.WriteLine(logEntry); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to log to file: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/src/PodePwshMonitor/Service/PodePwshMain.cs b/src/PodePwshMonitor/Service/PodePwshMain.cs new file mode 100644 index 000000000..6da673778 --- /dev/null +++ b/src/PodePwshMonitor/Service/PodePwshMain.cs @@ -0,0 +1,109 @@ +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.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + + +namespace Pode.Service +{ + public static class Program + { + public static void Main(string[] args) + { + var customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Custom config file from args or default + + + // Retrieve the service name from the configuration + string serviceName = "PodeService"; + var config = new ConfigurationBuilder() + .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) + .Build(); + serviceName = config.GetSection("PodePwshWorker:Name").Value ?? serviceName; + Logger.Initialize(config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log", LogLevel.INFO); + + var builder = Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((context, config) => + { + // Load configuration from the specified JSON file + config.AddJsonFile(customConfigFile, optional: false, reloadOnChange: true); + }) + .ConfigureServices((context, services) => + { + // Bind the PodePwshWorkerOptions section from configuration + services.Configure(context.Configuration.GetSection("PodePwshWorker")); + + // Add the PodePwshWorker as a hosted service + services.AddHostedService(); + +#if WINDOWS + // Register PodePwshMonitor as a singleton with proper error handling + services.AddSingleton(serviceProvider => + { + try + { + // Retrieve worker options from the service provider + var options = serviceProvider.GetRequiredService>().Value; + + // Log the options for debugging + Logger.Log(LogLevel.INFO,"Server","Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); + + // Return the configured PodePwshMonitor instance + return new PodePwshMonitor(options); + } + catch (Exception ex) + { + // Log and write critical errors to the Event Log + Logger.Log(LogLevel.ERROR,ex, "Failed to initialize PodePwshMonitor."); + + + throw; // Rethrow to terminate the application + } + }); + + // Register IPausableHostedService for handling pause and continue operations + services.AddSingleton(); +#endif + }); + + + // Check if running on Linux and use Systemd + if (OperatingSystem.IsLinux()) + { + builder.UseSystemd(); + builder.Build().Run(); + } + // Check if running on Windows and use Windows Service + else if (OperatingSystem.IsWindows()) + { + //builder.UseWindowsService(); + // Windows-specific logic for CanPauseAndContinue +#if WINDOWS + using var host = builder.Build(); + var service = new PodeWindowsService(host,serviceName); + ServiceBase.Run(service); +#else + builder.UseWindowsService(); + builder.Build().Run(); +#endif + } + else if (OperatingSystem.IsMacOS()) + { + // No specific macOS service manager, it runs under launchd + builder.Build().Run(); + } + else + { + // Fallback for unsupported platforms + Logger.Log(LogLevel.WARN, "Server", "Unsupported platform. Exiting."); + return; + } + + } + } +} diff --git a/src/PodePwshMonitor/PodePwshMonitor.cs b/src/PodePwshMonitor/Service/PodePwshMonitor.cs similarity index 58% rename from src/PodePwshMonitor/PodePwshMonitor.cs rename to src/PodePwshMonitor/Service/PodePwshMonitor.cs index 5a7820a3b..310e5e46b 100644 --- a/src/PodePwshMonitor/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/Service/PodePwshMonitor.cs @@ -1,7 +1,7 @@ /* * PodePwshMonitorService * - * This service monitors and controls the execution of a PowerShell process using named pipes for communication. + * This service monitors and controls the execution of a Pode process using named pipes for communication. * * SC Command Reference for Managing Windows Services: * @@ -36,8 +36,9 @@ using System.IO.Pipes; using System.Threading.Tasks; using System.Threading; +using Microsoft.Extensions.Options; -namespace Pode.Services +namespace Pode.Service { public class PodePwshMonitor { @@ -52,81 +53,85 @@ public class PodePwshMonitor private NamedPipeClientStream _pipeClient; // Changed to client stream private DateTime _lastLogTime; - // private static string _logFilePath; // Path to log file + public int StartMaxRetryCount { get; private set; } // Maximum number of retries before breaking + public int StartRetryDelayMs { get; private set; } // Delay between retries in milliseconds - public PodePwshMonitor(string scriptPath, string pwshPath, string parameterString = "", string logFilePath = ".\\PodePwshMonitorService.log", bool quiet = true, bool disableTermination = true, int shutdownWaitTimeMs = 30000) + public PodePwshMonitor(PodePwshWorkerOptions options) { - Console.WriteLine("logFilePath{0}", logFilePath); + Console.WriteLine("logFilePath{0}", options.LogFilePath); // Initialize fields with constructor arguments - _scriptPath = scriptPath; // Path to the PowerShell script to be executed - _pwshPath = pwshPath; // Path to the PowerShell executable (pwsh) - _parameterString = parameterString; // Additional parameters to pass to the script (if any) - _disableTermination = disableTermination; // Flag to disable termination of the service - _quiet = quiet; // Flag to suppress output for a quieter service - _shutdownWaitTimeMs = shutdownWaitTimeMs; // Maximum wait time before forcefully shutting down the process - // _logFilePath = logFilePath ?? ".\\PodePwshMonitorService.log"; // Default to local file if none provided + _scriptPath = options.ScriptPath; // Path to the Pode script to be executed + _pwshPath = options.PwshPath; // Path to the Pode executable (pwsh) + _parameterString = options.ParameterString; // Additional parameters to pass to the script (if any) + _disableTermination = options.DisableTermination; // Flag to disable termination of the service + _quiet = options.Quiet; // Flag to suppress output for a quieter service + _shutdownWaitTimeMs = options.ShutdownWaitTimeMs; // Maximum wait time before forcefully shutting down the process + StartMaxRetryCount = options.StartMaxRetryCount; + StartRetryDelayMs = options.StartRetryDelayMs; // Dynamically generate a unique PipeName for communication _pipeName = $"PodePipe_{Guid.NewGuid()}"; // Generate a unique pipe name to avoid conflicts } + public void StartPowerShellProcess() { if (_powerShellProcess == null || _powerShellProcess.HasExited) { try { - // Define the PowerShell process + // Define the Pode process _powerShellProcess = new Process { StartInfo = new ProcessStartInfo { - FileName = _pwshPath, // Set the PowerShell executable path (pwsh) + FileName = _pwshPath, // Set the Pode executable path (pwsh) RedirectStandardOutput = true, // Redirect standard output RedirectStandardError = true, // Redirect standard error UseShellExecute = false, // Do not use shell execution CreateNoWindow = true // Do not create a new window } }; - Program.Log($"Starting ..."); + Logger.Log(LogLevel.INFO, "Server", $"Starting ..."); // Properly escape double quotes within the JSON string string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; - Program.Log($"Powershell path {_pwshPath}"); - Program.Log($"PodeService content:"); - Program.Log($"DisableTermination\t= {_disableTermination.ToString().ToLower()}"); - Program.Log($"Quiet\t= {_quiet.ToString().ToLower()}"); - Program.Log($"PipeName\t= {_pipeName}"); - // Build the PowerShell command with NoProfile and global variable initialization + Logger.Log(LogLevel.INFO, "Server", $"Pode path {_pwshPath}"); + Logger.Log(LogLevel.INFO, "Server", $"PodeService content:"); + Logger.Log(LogLevel.INFO, "Server", $"DisableTermination\t= {_disableTermination.ToString().ToLower()}"); + Logger.Log(LogLevel.INFO, "Server", $"Quiet\t= {_quiet.ToString().ToLower()}"); + Logger.Log(LogLevel.INFO, "Server", $"PipeName\t= {_pipeName}"); + // Build the Pode command with NoProfile and global variable initialization string command = $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; - Program.Log($"Starting PowerShell process with command: {command}"); + Logger.Log(LogLevel.INFO, "Server", $"Starting Pode process with command: {command}"); - // Set the arguments for the PowerShell process + // Set the arguments for the Pode process _powerShellProcess.StartInfo.Arguments = command; // Start the process _powerShellProcess.Start(); // Log output and error asynchronously - _powerShellProcess.OutputDataReceived += (sender, args) => Program.Log(args.Data); - _powerShellProcess.ErrorDataReceived += (sender, args) => Program.Log(args.Data); + _powerShellProcess.OutputDataReceived += (sender, args) => Logger.Log(LogLevel.INFO, "Server", args.Data); + _powerShellProcess.ErrorDataReceived += (sender, args) => Logger.Log(LogLevel.INFO, "Server", args.Data); _powerShellProcess.BeginOutputReadLine(); _powerShellProcess.BeginErrorReadLine(); _lastLogTime = DateTime.Now; - Program.Log("PowerShell process started successfully."); + Logger.Log(LogLevel.INFO, "Server", "Pode process started successfully."); } catch (Exception ex) { - Program.Log($"Failed to start PowerShell process: {ex.Message}"); + Logger.Log(LogLevel.ERROR, "Server", $"Failed to start Pode process: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); } } else { // Log only if more than a minute has passed since the last log - if ((DateTime.Now - _lastLogTime).TotalMinutes >= 1) + if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) { - Program.Log("PowerShell process is already running."); + Logger.Log(LogLevel.INFO, "Server", "Pode process is Alive."); _lastLogTime = DateTime.Now; } } @@ -137,16 +142,16 @@ public void StopPowerShellProcess() try { _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Program.Log($"Connecting to the pipe server using pipe: {_pipeName}"); + Logger.Log(LogLevel.INFO, "Server", $"Connecting to the pipe server using pipe: {_pipeName}"); - // Connect to the PowerShell pipe server + // Connect to the Pode pipe server _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection if (_pipeClient.IsConnected) { // Send shutdown message and wait for the process to exit SendPipeMessage("shutdown"); - Program.Log($"Waiting up to {_shutdownWaitTimeMs} milliseconds for the PowerShell process to exit..."); + Logger.Log(LogLevel.INFO, "Server", $"Waiting up to {_shutdownWaitTimeMs} milliseconds for the Pode process to exit..."); // Timeout logic int waited = 0; @@ -160,16 +165,16 @@ public void StopPowerShellProcess() if (_powerShellProcess.HasExited) { - Program.Log("PowerShell process has been shutdown gracefully."); + Logger.Log(LogLevel.INFO, "Server", "Pode process has been shutdown gracefully."); } else { - Program.Log($"PowerShell process did not exit in {_shutdownWaitTimeMs} milliseconds."); + Logger.Log(LogLevel.WARN, "Server", $"Pode process did not exit in {_shutdownWaitTimeMs} milliseconds."); } } else { - Program.Log($"Failed to connect to the PowerShell pipe server using pipe: {_pipeName}"); + Logger.Log(LogLevel.ERROR, "Server", $"Failed to connect to the Pode pipe server using pipe: {_pipeName}"); } // Forcefully kill the process if it's still running @@ -178,17 +183,20 @@ public void StopPowerShellProcess() try { _powerShellProcess.Kill(); - Program.Log("PowerShell process killed successfully."); + Logger.Log(LogLevel.INFO, "Server", "Pode process killed successfully."); } catch (Exception ex) { - Program.Log($"Error killing PowerShell process: {ex.Message}"); + Logger.Log(LogLevel.ERROR, "Server", $"Error killing Pode process: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); } } } catch (Exception ex) { - Program.Log($"Error stopping PowerShell process: {ex.Message}"); + Logger.Log(LogLevel.ERROR, "Server", $"Error stopping Pode process: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); + } finally { @@ -204,8 +212,8 @@ public void StopPowerShellProcess() _pipeClient?.Dispose(); _pipeClient = null; } - Program.Log("PowerShell process and pipe client disposed."); - Program.Log("Done."); + Logger.Log(LogLevel.DEBUG, "Server", "Pode process and pipe client disposed."); + Logger.Log(LogLevel.INFO, "Server", "Done."); } } @@ -214,8 +222,8 @@ public void RestartPowerShellProcess() // Simply send the restart message, no need to stop and start again if (_pipeClient != null && _pipeClient.IsConnected) { - SendPipeMessage("restart"); // Inform PowerShell about the restart - Program.Log("Restart message sent to PowerShell."); + SendPipeMessage("restart"); // Inform Pode about the restart + Logger.Log(LogLevel.INFO, "Server", "Restart message sent to PowerShell."); } } @@ -224,21 +232,21 @@ public void SuspendPowerShellProcess() try { _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Program.Log($"Connecting to the pipe server using pipe: {_pipeName}"); + Logger.Log(LogLevel.INFO, "Server", $"Connecting to the pipe server using pipe: {_pipeName}"); - // Connect to the PowerShell pipe server - _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection + // Connect to the Pode pipe server + _pipeClient.Connect(20000); // Wait for up to 10 seconds for the connection // Simply send the restart message, no need to stop and start again - if (_pipeClient != null && _pipeClient.IsConnected) + if (_pipeClient.IsConnected) { - Program.Log("maybe I'm here too."); - SendPipeMessage("suspend"); // Inform PowerShell about the restart - Program.Log("Suspend message sent to PowerShell."); + SendPipeMessage("suspend"); // Inform Pode about the restart + Logger.Log(LogLevel.INFO, "Server", "Suspend message sent to PowerShell."); } } catch (Exception ex) { - Program.Log(ex, $"Error suspending PowerShell process"); + Logger.Log(LogLevel.ERROR, "Server", $"Error suspending Pode process: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); } finally { @@ -248,8 +256,8 @@ public void SuspendPowerShellProcess() _pipeClient?.Dispose(); _pipeClient = null; } - Program.Log("PowerShell process and pipe client disposed."); - Program.Log("Done."); + Logger.Log(LogLevel.DEBUG, "Server", "Pode process and pipe client disposed."); + Logger.Log(LogLevel.INFO, "Server", "Done."); } } @@ -258,20 +266,21 @@ public void ResumePowerShellProcess() try { _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Program.Log($"Connecting to the pipe server using pipe: {_pipeName}"); + Logger.Log(LogLevel.INFO, "Server", $"Connecting to the pipe server using pipe: {_pipeName}"); - // Connect to the PowerShell pipe server + // Connect to the Pode pipe server _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection // Simply send the restart message, no need to stop and start again if (_pipeClient != null && _pipeClient.IsConnected) { - SendPipeMessage("resume"); // Inform PowerShell about the restart - Program.Log("Resume message sent to PowerShell."); + SendPipeMessage("resume"); // Inform Pode about the restart + Logger.Log(LogLevel.INFO, "Server", "Resume message sent to PowerShell."); } } catch (Exception ex) { - Program.Log(ex, $"Error resuming PowerShell process"); + Logger.Log(LogLevel.ERROR, "Server", $"Error resuming Pode process: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); } finally { @@ -281,26 +290,26 @@ public void ResumePowerShellProcess() _pipeClient?.Dispose(); _pipeClient = null; } - Program.Log("PowerShell process and pipe client disposed."); - Program.Log("Done."); + Logger.Log(LogLevel.DEBUG, "Server", "Pode process and pipe client disposed."); + Logger.Log(LogLevel.INFO, "Server", "Done."); } } private void SendPipeMessage(string message) { - Program.Log("SendPipeMessage: {0}", message); + Logger.Log(LogLevel.INFO, "Server", "SendPipeMessage: {0}", message); // Write the message to the pipe if (_pipeClient == null) { - Program.Log("Pipe client is not initialized, cannot send message."); + Logger.Log(LogLevel.ERROR, "Server", "Pipe client is not initialized, cannot send message."); return; } if (!_pipeClient.IsConnected) { - Program.Log("Pipe client is not connected, cannot send message."); + Logger.Log(LogLevel.ERROR, "Server", "Pipe client is not connected, cannot send message."); return; } @@ -310,11 +319,12 @@ private void SendPipeMessage(string message) using var writer = new StreamWriter(_pipeClient, leaveOpen: true); // leaveOpen to keep the pipe alive for multiple writes writer.AutoFlush = true; writer.WriteLine(message); - Program.Log($"Message sent to PowerShell: {message}"); + Logger.Log(LogLevel.INFO, "Server", $"Message sent to PowerShell: {message}"); } catch (Exception ex) { - Program.Log($"Failed to send message to PowerShell: {ex.Message}"); + Logger.Log(LogLevel.ERROR, "Server", $"Failed to send message to PowerShell: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); } } diff --git a/src/PodePwshMonitor/Service/PodePwshWorker.cs b/src/PodePwshMonitor/Service/PodePwshWorker.cs new file mode 100644 index 000000000..8a07c6d0a --- /dev/null +++ b/src/PodePwshMonitor/Service/PodePwshWorker.cs @@ -0,0 +1,159 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Pode.Service +{ + public sealed class PodePwshWorker : BackgroundService, IPausableHostedService + { + private readonly ILogger _logger; + private PodePwshMonitor _pwshMonitor; + + private volatile bool _isPaused; + + private int _delayMs = 5000; // 5 seconds delay + + public PodePwshWorker(ILogger logger, PodePwshMonitor pwshMonitor) + { + _logger = logger; + _pwshMonitor = pwshMonitor; // Shared instance + _logger.LogInformation("PodePwshWorker initialized with shared PodePwshMonitor."); + } + + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Logger.Log(LogLevel.INFO, "Server", "PodePwshWorker running at: {0}", DateTimeOffset.Now); + + int retryCount = 0; + + while (!stoppingToken.IsCancellationRequested) + { + if (_isPaused) + { + Logger.Log(LogLevel.INFO, "Server", "Worker is paused. Waiting..."); + await Task.Delay(1000, stoppingToken); // Pause handling + continue; + } + try + { + // Reset retry count on successful execution + retryCount = 0; + + // Start the PowerShell process + _pwshMonitor.StartPowerShellProcess(); + + } + catch (Exception ex) + { + retryCount++; + Logger.Log(LogLevel.ERROR, ex, "An error occurred in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); + + // Check if maximum retries have been reached + if (retryCount >= _pwshMonitor.StartMaxRetryCount) + { + Logger.Log(LogLevel.CRITICAL, "Maximum retry count reached. Breaking the monitoring loop."); + break; // Exit the loop + } + + // Wait for a while before retrying + try + { + await Task.Delay(_pwshMonitor.StartRetryDelayMs, stoppingToken); + } + catch (OperationCanceledException) + { + Logger.Log(LogLevel.WARN, "Server", "Operation canceled during retry delay."); + break; // Exit the loop if the operation is canceled + } + } + // Wait before the next monitoring iteration + await Task.Delay(10000, stoppingToken); + } + + Logger.Log(LogLevel.INFO, "Server", "Monitoring loop has stopped."); + } + + + public override async Task StopAsync(CancellationToken stoppingToken) + { + Logger.Log(LogLevel.INFO, "Server", "Service is stopping at: {0}", DateTimeOffset.Now); + try + { + _pwshMonitor.StopPowerShellProcess(); + } + catch (Exception ex) + { + Logger.Log(LogLevel.ERROR, ex, "Error while stopping PowerShell process: {message}", ex.Message); + } + // Wait for the base StopAsync to complete + await base.StopAsync(stoppingToken); + + Logger.Log(LogLevel.INFO, "Server", "Service stopped successfully at: {0}", DateTimeOffset.Now); + + } + + // Custom RestartAsync method that sends a restart message via pipe + public void RestartAsync() + { + Logger.Log(LogLevel.INFO, "Server", "Service is restarting at: {0}", DateTimeOffset.Now); + try + { + // Send the 'restart' message using the pipe + _pwshMonitor.RestartPowerShellProcess(); + + Logger.Log(LogLevel.INFO, "Server", "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + + } + catch (Exception ex) + { + Logger.Log(LogLevel.ERROR, ex, "An error occurred during restart: {message}", ex.Message); + } + } + + + public void OnPause() + { + Logger.Log(LogLevel.INFO, "Server", "Pause command received at: {0}", DateTimeOffset.Now); + + try + { + _pwshMonitor.SuspendPowerShellProcess(); + _isPaused = true; + Logger.Log(LogLevel.INFO, "Server", "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + + // Add delay to prevent rapid consecutive operations + Logger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); + Thread.Sleep(_delayMs); + } + catch (Exception ex) + { + Logger.Log(LogLevel.ERROR, ex, "Error occurred while suspending PowerShell process: {message}", ex.Message); + } + } + + public void OnContinue() + { + Logger.Log(LogLevel.INFO, "Server", "Continue command received at: {0}", DateTimeOffset.Now); + + try + { + _pwshMonitor.ResumePowerShellProcess(); + _isPaused = false; + Logger.Log(LogLevel.INFO, "Server", "Resume message sent via pipe at: {0}", DateTimeOffset.Now); + + // Add delay to prevent rapid consecutive operations + Logger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); + Thread.Sleep(_delayMs); + } + catch (Exception ex) + { + Logger.Log(LogLevel.ERROR, ex, "Error occurred while resuming PowerShell process: {message}", ex.Message); + } + } + } +} diff --git a/src/PodePwshMonitor/PodePwshWorkerOptions.cs b/src/PodePwshMonitor/Service/PodePwshWorkerOptions.cs similarity index 99% rename from src/PodePwshMonitor/PodePwshWorkerOptions.cs rename to src/PodePwshMonitor/Service/PodePwshWorkerOptions.cs index e2efb2160..a628388b1 100644 --- a/src/PodePwshMonitor/PodePwshWorkerOptions.cs +++ b/src/PodePwshMonitor/Service/PodePwshWorkerOptions.cs @@ -1,6 +1,6 @@ using System; -namespace Pode.Services +namespace Pode.Service { /// /// Configuration options for the PodePwshWorker service. diff --git a/src/PodePwshMonitor/Service/PodeWindowsService.cs b/src/PodePwshMonitor/Service/PodeWindowsService.cs new file mode 100644 index 000000000..a9abf98c6 --- /dev/null +++ b/src/PodePwshMonitor/Service/PodeWindowsService.cs @@ -0,0 +1,86 @@ +#if WINDOWS +using System; +using Microsoft.Extensions.Hosting; +using System.ServiceProcess; +using System.Runtime.Versioning; +using System.Diagnostics; + + +namespace Pode.Service +{ + [SupportedOSPlatform("windows")] + public class PodeWindowsService : ServiceBase + { + private readonly IHost _host; + + + public PodeWindowsService(IHost host, string serviceName ) + { + _host = host; + CanPauseAndContinue = true; + ServiceName = serviceName; // Dynamically set the service name + } + + protected override void OnStart(string[] args) + { + Logger.Log(LogLevel.INFO,"Server","Service starting..."); + try{ + base.OnStart(args); + _host.StartAsync().Wait(); + Logger.Log(LogLevel.INFO,"Server","Service started successfully.");} + catch (Exception ex) + { + // Log the exception details to your custom log file + Logger.Log(LogLevel.ERROR,ex, "Service startup failed."); + + // Optionally write to the Windows Event Viewer for critical errors + 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; + } + } + + protected override void OnStop() + { + Logger.Log(LogLevel.INFO,"Server","Service stopping..."); + base.OnStop(); + _host.StopAsync().Wait(); + Logger.Log(LogLevel.INFO,"Server","Service stopped successfully."); + } + + protected override void OnPause() + { + Logger.Log(LogLevel.INFO,"Server","Service pausing..."); + base.OnPause(); + var service = _host.Services.GetService(typeof(IPausableHostedService)); + if (service != null) + { + Logger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnPause(); + } + else + { + Logger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); + } + } + + protected override void OnContinue() + { + Logger.Log(LogLevel.INFO,"Server","Service resuming..."); + base.OnContinue(); + var service = _host.Services.GetService(typeof(IPausableHostedService)); + if (service != null) + { + Logger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnContinue(); + } + else + { + Logger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index d01d8de5b..9487becb5 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -58,9 +58,9 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { - Write-PodeHost -Message "[Client] - Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" -Force while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + Write-PodeHost -Message "[Client] - Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" -Force try { Start-Sleep -Milliseconds 100 # Create a named pipe server stream @@ -108,7 +108,7 @@ function Start-PodeServiceHearthbeat { Write-PodeHost -Message '[Client] - Server requested suspend. Suspending client...' -Force Start-Sleep 5 #Suspend-PodeServer # Suspend Pode server - return # Exit the loop + # return # Exit the loop } 'resume' { @@ -116,7 +116,7 @@ function Start-PodeServiceHearthbeat { Write-PodeHost -Message '[Client] - Server requested resume. Resuming client...' -Force Start-Sleep 5 #Resume-PodeServer # Resume Pode server - return # Exit the loop + # return # Exit the loop } } @@ -131,6 +131,7 @@ function Start-PodeServiceHearthbeat { $reader.Dispose() $pipeStream.Dispose() # Always dispose of the pipe stream when done } + } } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 2a0818989..a727429e6 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -496,30 +496,31 @@ function Stop-PodeService { } + <# .SYNOPSIS - Stops a Pode-based service across different platforms (Windows, Linux, and macOS). + Suspends a specified service on Windows systems. .DESCRIPTION - The `Stop-PodeService` function stops a Pode-based service by checking if it is currently running. - If the service is running, it will attempt to stop the service gracefully. - The function works on Windows, Linux (systemd), and macOS (launchctl). + This function attempts to suspend a service by name. It is supported only on Windows systems. + On Linux and macOS, the suspend functionality for services is not available and an appropriate error message is returned. .PARAMETER Name - The name of the service. + The name of the service to suspend. .EXAMPLE - Stop-PodeService - - Stops the Pode-based service if it is currently running. If the service is not running, no action is taken. + Suspend-PodeService -Name 'MyService' .NOTES - - The function retrieves the service name from the `srvsettings.json` file located in the script directory. - - On Windows, it uses `Get-Service` and `Stop-Service`. - - On Linux, it uses `systemctl` to stop the service. - - On macOS, it uses `launchctl` to stop the service. - - If the service is not registered, the function throws an error. + This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. + +.EXCEPTION + Throws an exception if the service cannot be found, is not running, or if an error occurs while attempting to suspend the service. + +.SUPPORT + This function supports Windows only. #> + function Suspend-PodeService { param( [Parameter(Mandatory = $true)] @@ -532,12 +533,11 @@ function Suspend-PodeService { Confirm-PodeAdminPrivilege if ($IsWindows) { - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running if ($service.Status -eq 'Running') { - $null = Invoke-PodeWinElevatedCommand -Command 'Suspend-Service' -Arguments "-Name '$Name'" + $null = Invoke-PodeWinElevatedCommand -Command 'Suspend-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Paused') { Write-Verbose -Message "Service '$Name' suspended successfully." @@ -557,61 +557,9 @@ function Suspend-PodeService { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } } - elseif ($IsLinux) { - <# $nameService = "$Name.service".Replace(' ', '_') - # Check if the service is already registered - if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { - # Check if the service is active - if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { - #Stop the service - if (( Stop-PodeLinuxService -Name $nameService)) { - # Check if the service is active - if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - return $true - } - } - - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo launchctl stop', $Name) - } - else { - Write-Verbose -Message "Service '$Name' is not running." - } - } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) - } -#> - } - elseif ($IsMacOS) { - <# - $nameService = "pode.$Name.service".Replace(' ', '_') - # Check if the service is already registered - if ((Test-PodeMacOsServiceIsRegistered -Name $nameService)) { - # Check if the service is active - if ((Test-PodeMacOsServiceIsActive $nameService)) { - if ((Stop-PodeMacOsService $nameService)) { - if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - return $true - } - } - - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) - - } - else { - Write-Verbose -Message "Service '$Name' is not running." - } - } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) - } - #> + else { + # Feature not supported on Linux or macOS + throw ($PodeLocale.featureNotSupportedException -f 'Suspend Service') } } catch { @@ -624,6 +572,29 @@ function Suspend-PodeService { +<# +.SYNOPSIS + Resumes a specified service on Windows systems. + +.DESCRIPTION + This function attempts to resume a service by name. It is supported only on Windows systems. + On Linux and macOS, the resume functionality for services is not available, and an appropriate error message is returned. + +.PARAMETER Name + The name of the service to resume. + +.EXAMPLE + Resume-PodeService -Name 'MyService' + +.NOTES + This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. + +.EXCEPTION + Throws an exception if the service cannot be found, is not suspended, or if an error occurs while attempting to resume the service. + +.SUPPORT + This function supports Windows only. +#> function Resume-PodeService { param( [Parameter(Mandatory = $true)] @@ -661,61 +632,10 @@ function Resume-PodeService { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } } - elseif ($IsLinux) { - <# $nameService = "$Name.service".Replace(' ', '_') - # Check if the service is already registered - if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { - # Check if the service is active - if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { - #Stop the service - if (( Stop-PodeLinuxService -Name $nameService)) { - # Check if the service is active - if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - return $true - } - } - - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo launchctl stop', $Name) - } - else { - Write-Verbose -Message "Service '$Name' is not running." - } - } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) - } -#> - } - elseif ($IsMacOS) { - <# - $nameService = "pode.$Name.service".Replace(' ', '_') - # Check if the service is already registered - if ((Test-PodeMacOsServiceIsRegistered -Name $nameService)) { - # Check if the service is active - if ((Test-PodeMacOsServiceIsActive $nameService)) { - if ((Stop-PodeMacOsService $nameService)) { - if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - return $true - } - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) - - } - else { - Write-Verbose -Message "Service '$Name' is not running." - } - } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) - } - #> + else { + # Feature not supported on Linux or macOS + throw ($PodeLocale.featureNotSupportedException -f 'Resume Service') } } catch { From c11eb7fc4e410ce6e22ac832ecfcdd2ce69d65f3 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 17 Nov 2024 10:09:50 -0800 Subject: [PATCH 31/93] revert to net8 --- src/PodePwshMonitor/PodeMonitor.csproj | 15 +- .../Service/PodePwshMonitor.cs | 345 +++++++----------- 2 files changed, 130 insertions(+), 230 deletions(-) diff --git a/src/PodePwshMonitor/PodeMonitor.csproj b/src/PodePwshMonitor/PodeMonitor.csproj index abbe53c35..f5ccbbef9 100644 --- a/src/PodePwshMonitor/PodeMonitor.csproj +++ b/src/PodePwshMonitor/PodeMonitor.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net8.0 true true true @@ -10,12 +10,13 @@ - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/PodePwshMonitor/Service/PodePwshMonitor.cs b/src/PodePwshMonitor/Service/PodePwshMonitor.cs index 310e5e46b..e11c63d7f 100644 --- a/src/PodePwshMonitor/Service/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/Service/PodePwshMonitor.cs @@ -1,119 +1,79 @@ -/* - * PodePwshMonitorService - * - * This service monitors and controls the execution of a Pode process using named pipes for communication. - * - * SC Command Reference for Managing Windows Services: - * - * Install Service: - * sc create PodePwshMonitorService binPath= "C:\path\to\your\service\PodePwshMonitorService.exe" start= auto - * - * Start Service: - * sc start PodePwshMonitorService - * - * Stop Service: - * sc stop PodePwshMonitorService - * - * Delete Service: - * sc delete PodePwshMonitorService - * - * Query Service Status: - * sc query PodePwshMonitorService - * - * Configure Service to Restart on Failure: - * sc failure PodePwshMonitorService reset= 0 actions= restart/60000 - * - * Example for running the service: - * sc start PodePwshMonitorService - * sc stop PodePwshMonitorService - * sc delete PodePwshMonitorService - * - */ using System; -using Microsoft.Extensions.Hosting; using System.Diagnostics; using System.IO; using System.IO.Pipes; -using System.Threading.Tasks; using System.Threading; -using Microsoft.Extensions.Options; namespace Pode.Service { public class PodePwshMonitor { + private readonly object _syncLock = new(); // Synchronization lock for thread safety private Process _powerShellProcess; + private NamedPipeClientStream _pipeClient; + private readonly string _scriptPath; private readonly string _parameterString; - private string _pwshPath; + private readonly string _pwshPath; private readonly bool _quiet; private readonly bool _disableTermination; private readonly int _shutdownWaitTimeMs; - private string _pipeName; - private NamedPipeClientStream _pipeClient; // Changed to client stream + private readonly string _pipeName; + private DateTime _lastLogTime; - public int StartMaxRetryCount { get; private set; } // Maximum number of retries before breaking - public int StartRetryDelayMs { get; private set; } // Delay between retries in milliseconds + public int StartMaxRetryCount { get; } + public int StartRetryDelayMs { get; } public PodePwshMonitor(PodePwshWorkerOptions options) { - Console.WriteLine("logFilePath{0}", options.LogFilePath); - // Initialize fields with constructor arguments - _scriptPath = options.ScriptPath; // Path to the Pode script to be executed - _pwshPath = options.PwshPath; // Path to the Pode executable (pwsh) - _parameterString = options.ParameterString; // Additional parameters to pass to the script (if any) - _disableTermination = options.DisableTermination; // Flag to disable termination of the service - _quiet = options.Quiet; // Flag to suppress output for a quieter service - _shutdownWaitTimeMs = options.ShutdownWaitTimeMs; // Maximum wait time before forcefully shutting down the process + _scriptPath = options.ScriptPath; + _pwshPath = options.PwshPath; + _parameterString = options.ParameterString; + _quiet = options.Quiet; + _disableTermination = options.DisableTermination; + _shutdownWaitTimeMs = options.ShutdownWaitTimeMs; StartMaxRetryCount = options.StartMaxRetryCount; StartRetryDelayMs = options.StartRetryDelayMs; - // Dynamically generate a unique PipeName for communication - _pipeName = $"PodePipe_{Guid.NewGuid()}"; // Generate a unique pipe name to avoid conflicts + _pipeName = $"PodePipe_{Guid.NewGuid()}"; + Logger.Log(LogLevel.INFO, "Server", $"Initialized PodePwshMonitor with pipe name: {_pipeName}"); } - public void StartPowerShellProcess() { - if (_powerShellProcess == null || _powerShellProcess.HasExited) + lock (_syncLock) { + if (_powerShellProcess != null && !_powerShellProcess.HasExited) + { + // Log only if more than a minute has passed since the last log + if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) + { + Logger.Log(LogLevel.INFO, "Server", "Pode process is Alive."); + _lastLogTime = DateTime.Now; + } + return; + } + try { - // Define the Pode process _powerShellProcess = new Process { StartInfo = new ProcessStartInfo { - FileName = _pwshPath, // Set the Pode executable path (pwsh) - RedirectStandardOutput = true, // Redirect standard output - RedirectStandardError = true, // Redirect standard error - UseShellExecute = false, // Do not use shell execution - CreateNoWindow = true // Do not create a new window + FileName = _pwshPath, + Arguments = BuildCommand(), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true } }; - Logger.Log(LogLevel.INFO, "Server", $"Starting ..."); - // Properly escape double quotes within the JSON string - string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; - Logger.Log(LogLevel.INFO, "Server", $"Pode path {_pwshPath}"); - Logger.Log(LogLevel.INFO, "Server", $"PodeService content:"); - Logger.Log(LogLevel.INFO, "Server", $"DisableTermination\t= {_disableTermination.ToString().ToLower()}"); - Logger.Log(LogLevel.INFO, "Server", $"Quiet\t= {_quiet.ToString().ToLower()}"); - Logger.Log(LogLevel.INFO, "Server", $"PipeName\t= {_pipeName}"); - // Build the Pode command with NoProfile and global variable initialization - string command = $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; - Logger.Log(LogLevel.INFO, "Server", $"Starting Pode process with command: {command}"); - - // Set the arguments for the Pode process - _powerShellProcess.StartInfo.Arguments = command; + _powerShellProcess.OutputDataReceived += (sender, args) => Logger.Log(LogLevel.INFO, "Server", args.Data); + _powerShellProcess.ErrorDataReceived += (sender, args) => Logger.Log(LogLevel.ERROR, "Server", args.Data); - // Start the process _powerShellProcess.Start(); - - // Log output and error asynchronously - _powerShellProcess.OutputDataReceived += (sender, args) => Logger.Log(LogLevel.INFO, "Server", args.Data); - _powerShellProcess.ErrorDataReceived += (sender, args) => Logger.Log(LogLevel.INFO, "Server", args.Data); _powerShellProcess.BeginOutputReadLine(); _powerShellProcess.BeginErrorReadLine(); @@ -126,207 +86,146 @@ public void StartPowerShellProcess() Logger.Log(LogLevel.DEBUG, ex); } } - else - { - // Log only if more than a minute has passed since the last log - if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) - { - Logger.Log(LogLevel.INFO, "Server", "Pode process is Alive."); - _lastLogTime = DateTime.Now; - } - } } public void StopPowerShellProcess() { - try + lock (_syncLock) { - _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Logger.Log(LogLevel.INFO, "Server", $"Connecting to the pipe server using pipe: {_pipeName}"); - - // Connect to the Pode pipe server - _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection - - if (_pipeClient.IsConnected) - { - // Send shutdown message and wait for the process to exit - SendPipeMessage("shutdown"); - Logger.Log(LogLevel.INFO, "Server", $"Waiting up to {_shutdownWaitTimeMs} milliseconds for the Pode process to exit..."); - - // Timeout logic - int waited = 0; - int interval = 200; // Check every 200ms - - while (!_powerShellProcess.HasExited && waited < _shutdownWaitTimeMs) - { - Thread.Sleep(interval); - waited += interval; - } - - if (_powerShellProcess.HasExited) - { - Logger.Log(LogLevel.INFO, "Server", "Pode process has been shutdown gracefully."); - } - else - { - Logger.Log(LogLevel.WARN, "Server", $"Pode process did not exit in {_shutdownWaitTimeMs} milliseconds."); - } - } - else + if (_powerShellProcess == null || _powerShellProcess.HasExited) { - Logger.Log(LogLevel.ERROR, "Server", $"Failed to connect to the Pode pipe server using pipe: {_pipeName}"); + Logger.Log(LogLevel.INFO, "Server", "Pode process is not running."); + return; } - // Forcefully kill the process if it's still running - if (_powerShellProcess != null && !_powerShellProcess.HasExited) + try { - try + if (InitializePipeClient()) { - _powerShellProcess.Kill(); - Logger.Log(LogLevel.INFO, "Server", "Pode process killed successfully."); - } - catch (Exception ex) - { - Logger.Log(LogLevel.ERROR, "Server", $"Error killing Pode process: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); + SendPipeMessage("shutdown"); + + Logger.Log(LogLevel.INFO, "Server", $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); + WaitForProcessExit(_shutdownWaitTimeMs); + + if (!_powerShellProcess.HasExited) + { + Logger.Log(LogLevel.WARN, "Server", "Pode process did not terminate gracefully, killing process."); + _powerShellProcess.Kill(); + } + + Logger.Log(LogLevel.INFO, "Server", "Pode process stopped successfully."); } } - } - catch (Exception ex) - { - Logger.Log(LogLevel.ERROR, "Server", $"Error stopping Pode process: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); - - } - finally - { - // Set _powerShellProcess to null only if it's still not null - if (_powerShellProcess != null) + catch (Exception ex) { - _powerShellProcess?.Dispose(); - _powerShellProcess = null; + Logger.Log(LogLevel.ERROR, "Server", $"Error stopping Pode process: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); } - // Clean up the pipe client - if (_pipeClient != null) + finally { - _pipeClient?.Dispose(); - _pipeClient = null; + CleanupResources(); } - Logger.Log(LogLevel.DEBUG, "Server", "Pode process and pipe client disposed."); - Logger.Log(LogLevel.INFO, "Server", "Done."); } } - public void RestartPowerShellProcess() + public void SuspendPowerShellProcess() { - // Simply send the restart message, no need to stop and start again - if (_pipeClient != null && _pipeClient.IsConnected) - { - SendPipeMessage("restart"); // Inform Pode about the restart - Logger.Log(LogLevel.INFO, "Server", "Restart message sent to PowerShell."); - } + ExecutePipeCommand("suspend"); } - public void SuspendPowerShellProcess() + public void ResumePowerShellProcess() { - try - { - _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Logger.Log(LogLevel.INFO, "Server", $"Connecting to the pipe server using pipe: {_pipeName}"); + ExecutePipeCommand("resume"); + } - // Connect to the Pode pipe server - _pipeClient.Connect(20000); // Wait for up to 10 seconds for the connection - // Simply send the restart message, no need to stop and start again - if (_pipeClient.IsConnected) - { - SendPipeMessage("suspend"); // Inform Pode about the restart - Logger.Log(LogLevel.INFO, "Server", "Suspend message sent to PowerShell."); - } - } - catch (Exception ex) - { - Logger.Log(LogLevel.ERROR, "Server", $"Error suspending Pode process: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); - } - finally - { - // Clean up the pipe client - if (_pipeClient != null) - { - _pipeClient?.Dispose(); - _pipeClient = null; - } - Logger.Log(LogLevel.DEBUG, "Server", "Pode process and pipe client disposed."); - Logger.Log(LogLevel.INFO, "Server", "Done."); - } + public void RestartPowerShellProcess() + { + ExecutePipeCommand("restart"); } - public void ResumePowerShellProcess() + private void ExecutePipeCommand(string command) { - try + lock (_syncLock) { - _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - Logger.Log(LogLevel.INFO, "Server", $"Connecting to the pipe server using pipe: {_pipeName}"); - - // Connect to the Pode pipe server - _pipeClient.Connect(10000); // Wait for up to 10 seconds for the connection - // Simply send the restart message, no need to stop and start again - if (_pipeClient != null && _pipeClient.IsConnected) + try { - SendPipeMessage("resume"); // Inform Pode about the restart - Logger.Log(LogLevel.INFO, "Server", "Resume message sent to PowerShell."); + if (InitializePipeClient()) + { + SendPipeMessage(command); + Logger.Log(LogLevel.INFO, "Server", $"{command.ToUpper()} command sent to Pode process."); + } } - } - catch (Exception ex) - { - Logger.Log(LogLevel.ERROR, "Server", $"Error resuming Pode process: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); - } - finally - { - // Clean up the pipe client - if (_pipeClient != null) + catch (Exception ex) + { + Logger.Log(LogLevel.ERROR, "Server", $"Error executing {command} command: {ex.Message}"); + Logger.Log(LogLevel.DEBUG, ex); + } + finally { - _pipeClient?.Dispose(); - _pipeClient = null; + CleanupPipeClient(); } - Logger.Log(LogLevel.DEBUG, "Server", "Pode process and pipe client disposed."); - Logger.Log(LogLevel.INFO, "Server", "Done."); } } - private void SendPipeMessage(string message) + private string BuildCommand() { - Logger.Log(LogLevel.INFO, "Server", "SendPipeMessage: {0}", message); - - // Write the message to the pipe + string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; + return $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; + } + private bool InitializePipeClient() + { if (_pipeClient == null) { - Logger.Log(LogLevel.ERROR, "Server", "Pipe client is not initialized, cannot send message."); - return; + _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); } if (!_pipeClient.IsConnected) { - Logger.Log(LogLevel.ERROR, "Server", "Pipe client is not connected, cannot send message."); - return; + Logger.Log(LogLevel.INFO, "Server", "Connecting to pipe server..."); + _pipeClient.Connect(10000); } + return _pipeClient.IsConnected; + } + + private void SendPipeMessage(string message) + { try { - // Send the message using the pipe client stream - using var writer = new StreamWriter(_pipeClient, leaveOpen: true); // leaveOpen to keep the pipe alive for multiple writes - writer.AutoFlush = true; + using var writer = new StreamWriter(_pipeClient) { AutoFlush = true }; writer.WriteLine(message); - Logger.Log(LogLevel.INFO, "Server", $"Message sent to PowerShell: {message}"); } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, "Server", $"Failed to send message to PowerShell: {ex.Message}"); + Logger.Log(LogLevel.ERROR, "Server", $"Error sending message to pipe: {ex.Message}"); Logger.Log(LogLevel.DEBUG, ex); } } + private void WaitForProcessExit(int timeout) + { + int waited = 0; + while (!_powerShellProcess.HasExited && waited < timeout) + { + Thread.Sleep(200); // Check every 200ms + waited += 200; + } + } + + private void CleanupResources() + { + _powerShellProcess?.Dispose(); + _powerShellProcess = null; + + CleanupPipeClient(); + } + + private void CleanupPipeClient() + { + _pipeClient?.Dispose(); + _pipeClient = null; + } + } } From 4996642393a2b65ca30184cca04c4046d3dd7df9 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 17 Nov 2024 10:23:14 -0800 Subject: [PATCH 32/93] improvements --- .../Service/{Logger.cs => PodePwshLogger.cs} | 7 +- src/PodePwshMonitor/Service/PodePwshMain.cs | 8 +- .../Service/PodePwshMonitor.cs | 153 +++++++++++++----- src/PodePwshMonitor/Service/PodePwshWorker.cs | 40 ++--- .../Service/PodeWindowsService.cs | 22 +-- src/Private/Service.ps1 | 16 +- src/Public/Service.ps1 | 19 +-- 7 files changed, 158 insertions(+), 107 deletions(-) rename src/PodePwshMonitor/Service/{Logger.cs => PodePwshLogger.cs} (94%) diff --git a/src/PodePwshMonitor/Service/Logger.cs b/src/PodePwshMonitor/Service/PodePwshLogger.cs similarity index 94% rename from src/PodePwshMonitor/Service/Logger.cs rename to src/PodePwshMonitor/Service/PodePwshLogger.cs index 33704e6f9..569f64eee 100644 --- a/src/PodePwshMonitor/Service/Logger.cs +++ b/src/PodePwshMonitor/Service/PodePwshLogger.cs @@ -16,7 +16,7 @@ public enum LogLevel CRITICAL // Critical errors indicating severe failures } - public static class Logger + public static class PodePwshLogger { private static readonly object _logLock = new(); private static string logFilePath = "PodeService.log"; // Default log file path @@ -52,7 +52,7 @@ public static void Initialize(string filePath, LogLevel level) } } - public static void Log(LogLevel level, string context = "Client", string message = "", params object[] args) + public static void Log(LogLevel level, string context , string message = "", params object[] args) { if (level < minLogLevel || string.IsNullOrEmpty(message)) { @@ -64,9 +64,6 @@ public static void Log(LogLevel level, string context = "Client", string message // Format the message with the provided arguments var formattedMessage = string.Format(message, args); - // Ensure context is not null or empty - context = string.IsNullOrWhiteSpace(context) ? "Client" : context; - // Get the current time in ISO 8601 format in GMT/UTC string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); diff --git a/src/PodePwshMonitor/Service/PodePwshMain.cs b/src/PodePwshMonitor/Service/PodePwshMain.cs index 6da673778..a9109e6fa 100644 --- a/src/PodePwshMonitor/Service/PodePwshMain.cs +++ b/src/PodePwshMonitor/Service/PodePwshMain.cs @@ -25,7 +25,7 @@ public static void Main(string[] args) .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) .Build(); serviceName = config.GetSection("PodePwshWorker:Name").Value ?? serviceName; - Logger.Initialize(config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log", LogLevel.INFO); + PodePwshLogger.Initialize(config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log", LogLevel.INFO); var builder = Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => @@ -51,7 +51,7 @@ public static void Main(string[] args) var options = serviceProvider.GetRequiredService>().Value; // Log the options for debugging - Logger.Log(LogLevel.INFO,"Server","Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); + PodePwshLogger.Log(LogLevel.INFO,"Server","Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); // Return the configured PodePwshMonitor instance return new PodePwshMonitor(options); @@ -59,7 +59,7 @@ public static void Main(string[] args) catch (Exception ex) { // Log and write critical errors to the Event Log - Logger.Log(LogLevel.ERROR,ex, "Failed to initialize PodePwshMonitor."); + PodePwshLogger.Log(LogLevel.ERROR,ex, "Failed to initialize PodePwshMonitor."); throw; // Rethrow to terminate the application @@ -100,7 +100,7 @@ public static void Main(string[] args) else { // Fallback for unsupported platforms - Logger.Log(LogLevel.WARN, "Server", "Unsupported platform. Exiting."); + PodePwshLogger.Log(LogLevel.WARN, "Server", "Unsupported platform. Exiting."); return; } diff --git a/src/PodePwshMonitor/Service/PodePwshMonitor.cs b/src/PodePwshMonitor/Service/PodePwshMonitor.cs index e11c63d7f..7f4d29bc9 100644 --- a/src/PodePwshMonitor/Service/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/Service/PodePwshMonitor.cs @@ -6,27 +6,37 @@ namespace Pode.Service { + /// + /// The PodePwshMonitor class monitors and controls the execution of a Pode PowerShell process. + /// It communicates with the Pode process using named pipes. + /// public class PodePwshMonitor { private readonly object _syncLock = new(); // Synchronization lock for thread safety - private Process _powerShellProcess; - private NamedPipeClientStream _pipeClient; + private Process _powerShellProcess; // PowerShell process instance + private NamedPipeClientStream _pipeClient; // Named pipe client for communication - private readonly string _scriptPath; - private readonly string _parameterString; - private readonly string _pwshPath; - private readonly bool _quiet; - private readonly bool _disableTermination; - private readonly int _shutdownWaitTimeMs; - private readonly string _pipeName; + // Configuration properties + private readonly string _scriptPath; // Path to the Pode script + private readonly string _parameterString; // Parameters to pass to the script + private readonly string _pwshPath; // Path to the PowerShell executable + private readonly bool _quiet; // Whether the process runs in quiet mode + private readonly bool _disableTermination; // Whether termination is disabled + private readonly int _shutdownWaitTimeMs; // Timeout for shutting down the process + private readonly string _pipeName; // Name of the pipe for interprocess communication - private DateTime _lastLogTime; + private DateTime _lastLogTime; // Last log timestamp - public int StartMaxRetryCount { get; } - public int StartRetryDelayMs { get; } + public int StartMaxRetryCount { get; } // Maximum retries to start the process + public int StartRetryDelayMs { get; } // Delay between retries in milliseconds + /// + /// Initializes a new instance of the PodePwshMonitor class. + /// + /// The configuration options for the PodePwshWorker. public PodePwshMonitor(PodePwshWorkerOptions options) { + // Initialize configuration properties from options _scriptPath = options.ScriptPath; _pwshPath = options.PwshPath; _parameterString = options.ParameterString; @@ -36,20 +46,26 @@ public PodePwshMonitor(PodePwshWorkerOptions options) StartMaxRetryCount = options.StartMaxRetryCount; StartRetryDelayMs = options.StartRetryDelayMs; + // Generate a unique pipe name for communication _pipeName = $"PodePipe_{Guid.NewGuid()}"; - Logger.Log(LogLevel.INFO, "Server", $"Initialized PodePwshMonitor with pipe name: {_pipeName}"); + PodePwshLogger.Log(LogLevel.INFO, "Server", $"Initialized PodePwshMonitor with pipe name: {_pipeName}"); } + /// + /// Starts the Pode PowerShell process. + /// If the process is already running, logs its status. + /// public void StartPowerShellProcess() { - lock (_syncLock) + lock (_syncLock) // Ensure thread-safe access { + // Check if the process is already running if (_powerShellProcess != null && !_powerShellProcess.HasExited) { - // Log only if more than a minute has passed since the last log + // Log if the process is alive and log threshold is met if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) { - Logger.Log(LogLevel.INFO, "Server", "Pode process is Alive."); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process is Alive."); _lastLogTime = DateTime.Now; } return; @@ -57,122 +73,158 @@ public void StartPowerShellProcess() try { + // Configure the PowerShell process _powerShellProcess = new Process { StartInfo = new ProcessStartInfo { FileName = _pwshPath, Arguments = BuildCommand(), - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true + RedirectStandardOutput = true, // Redirect standard output + RedirectStandardError = true, // Redirect standard error + UseShellExecute = false, // Do not use shell execution + CreateNoWindow = true // Prevent creating a new window } }; - _powerShellProcess.OutputDataReceived += (sender, args) => Logger.Log(LogLevel.INFO, "Server", args.Data); - _powerShellProcess.ErrorDataReceived += (sender, args) => Logger.Log(LogLevel.ERROR, "Server", args.Data); + // Subscribe to output and error streams + _powerShellProcess.OutputDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.INFO, "Pode", args.Data); + _powerShellProcess.ErrorDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.ERROR, "Pode", args.Data); + // Start the process _powerShellProcess.Start(); _powerShellProcess.BeginOutputReadLine(); _powerShellProcess.BeginErrorReadLine(); + // Log the process start time _lastLogTime = DateTime.Now; - Logger.Log(LogLevel.INFO, "Server", "Pode process started successfully."); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process started successfully."); } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, "Server", $"Failed to start Pode process: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); + // Log any errors during process start + PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Failed to start Pode process: {ex.Message}"); + PodePwshLogger.Log(LogLevel.DEBUG, ex); } } } + /// + /// Stops the Pode PowerShell process gracefully. + /// If the process does not terminate gracefully, it will be forcefully terminated. + /// public void StopPowerShellProcess() { - lock (_syncLock) + lock (_syncLock) // Ensure thread-safe access { if (_powerShellProcess == null || _powerShellProcess.HasExited) { - Logger.Log(LogLevel.INFO, "Server", "Pode process is not running."); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process is not running."); return; } try { - if (InitializePipeClient()) + if (InitializePipeClient()) // Ensure pipe client is initialized { + // Send shutdown message and wait for process exit SendPipeMessage("shutdown"); - Logger.Log(LogLevel.INFO, "Server", $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); + // If process does not exit gracefully, forcefully terminate if (!_powerShellProcess.HasExited) { - Logger.Log(LogLevel.WARN, "Server", "Pode process did not terminate gracefully, killing process."); + PodePwshLogger.Log(LogLevel.WARN, "Server", "Pode process did not terminate gracefully, killing process."); _powerShellProcess.Kill(); } - Logger.Log(LogLevel.INFO, "Server", "Pode process stopped successfully."); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process stopped successfully."); } } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, "Server", $"Error stopping Pode process: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); + // Log errors during stop process + PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Error stopping Pode process: {ex.Message}"); + PodePwshLogger.Log(LogLevel.DEBUG, ex); } finally { + // Clean up resources 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) + lock (_syncLock) // Ensure thread-safe access { try { - if (InitializePipeClient()) + if (InitializePipeClient()) // Ensure pipe client is initialized { SendPipeMessage(command); - Logger.Log(LogLevel.INFO, "Server", $"{command.ToUpper()} command sent to Pode process."); + PodePwshLogger.Log(LogLevel.INFO, "Server", $"{command.ToUpper()} command sent to Pode process."); } } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, "Server", $"Error executing {command} command: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); + // Log errors during command execution + PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Error executing {command} command: {ex.Message}"); + PodePwshLogger.Log(LogLevel.DEBUG, ex); } finally { + // Clean up pipe client after sending the command CleanupPipeClient(); } } } + /// + /// Builds the PowerShell command to execute the Pode process. + /// + /// The PowerShell command string. private string BuildCommand() { string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; return $"-NoProfile -Command \"& {{ $global:PodeService = '{podeServiceJson}' | ConvertFrom-Json; . '{_scriptPath}' {_parameterString} }}\""; } + /// + /// Initializes the named pipe client for communication with the Pode process. + /// + /// True if the pipe client is successfully initialized and connected; otherwise, false. private bool InitializePipeClient() { if (_pipeClient == null) @@ -182,27 +234,35 @@ private bool InitializePipeClient() if (!_pipeClient.IsConnected) { - Logger.Log(LogLevel.INFO, "Server", "Connecting to pipe server..."); - _pipeClient.Connect(10000); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Connecting to pipe server..."); + _pipeClient.Connect(10000); // Connect with a timeout of 10 seconds } return _pipeClient.IsConnected; } + /// + /// 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); + writer.WriteLine(message); // Write the message to the pipe } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, "Server", $"Error sending message to pipe: {ex.Message}"); - Logger.Log(LogLevel.DEBUG, ex); + PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Error sending message to pipe: {ex.Message}"); + PodePwshLogger.Log(LogLevel.DEBUG, ex); } } + /// + /// Waits for the Pode process to exit within the specified timeout period. + /// + /// The timeout period in milliseconds. private void WaitForProcessExit(int timeout) { int waited = 0; @@ -213,6 +273,9 @@ private void WaitForProcessExit(int timeout) } } + /// + /// Cleans up resources associated with the Pode process and named pipe client. + /// private void CleanupResources() { _powerShellProcess?.Dispose(); @@ -221,11 +284,13 @@ private void CleanupResources() CleanupPipeClient(); } + /// + /// Cleans up the named pipe client. + /// private void CleanupPipeClient() { _pipeClient?.Dispose(); _pipeClient = null; } - } } diff --git a/src/PodePwshMonitor/Service/PodePwshWorker.cs b/src/PodePwshMonitor/Service/PodePwshWorker.cs index 8a07c6d0a..9e0e52d80 100644 --- a/src/PodePwshMonitor/Service/PodePwshWorker.cs +++ b/src/PodePwshMonitor/Service/PodePwshWorker.cs @@ -27,7 +27,7 @@ public PodePwshWorker(ILogger logger, PodePwshMonitor pwshMonito protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - Logger.Log(LogLevel.INFO, "Server", "PodePwshWorker running at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "PodePwshWorker running at: {0}", DateTimeOffset.Now); int retryCount = 0; @@ -35,7 +35,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (_isPaused) { - Logger.Log(LogLevel.INFO, "Server", "Worker is paused. Waiting..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Worker is paused. Waiting..."); await Task.Delay(1000, stoppingToken); // Pause handling continue; } @@ -51,12 +51,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) catch (Exception ex) { retryCount++; - Logger.Log(LogLevel.ERROR, ex, "An error occurred in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); + PodePwshLogger.Log(LogLevel.ERROR, ex, "An error occurred in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); // Check if maximum retries have been reached if (retryCount >= _pwshMonitor.StartMaxRetryCount) { - Logger.Log(LogLevel.CRITICAL, "Maximum retry count reached. Breaking the monitoring loop."); + PodePwshLogger.Log(LogLevel.CRITICAL, "Maximum retry count reached. Breaking the monitoring loop."); break; // Exit the loop } @@ -67,7 +67,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } catch (OperationCanceledException) { - Logger.Log(LogLevel.WARN, "Server", "Operation canceled during retry delay."); + PodePwshLogger.Log(LogLevel.WARN, "Server", "Operation canceled during retry delay."); break; // Exit the loop if the operation is canceled } } @@ -75,84 +75,84 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Task.Delay(10000, stoppingToken); } - Logger.Log(LogLevel.INFO, "Server", "Monitoring loop has stopped."); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Monitoring loop has stopped."); } public override async Task StopAsync(CancellationToken stoppingToken) { - Logger.Log(LogLevel.INFO, "Server", "Service is stopping at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service is stopping at: {0}", DateTimeOffset.Now); try { _pwshMonitor.StopPowerShellProcess(); } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, ex, "Error while stopping PowerShell process: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.ERROR, ex, "Error while stopping PowerShell process: {message}", ex.Message); } // Wait for the base StopAsync to complete await base.StopAsync(stoppingToken); - Logger.Log(LogLevel.INFO, "Server", "Service stopped successfully at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopped successfully at: {0}", DateTimeOffset.Now); } // Custom RestartAsync method that sends a restart message via pipe public void RestartAsync() { - Logger.Log(LogLevel.INFO, "Server", "Service is restarting at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service is restarting at: {0}", DateTimeOffset.Now); try { // Send the 'restart' message using the pipe _pwshMonitor.RestartPowerShellProcess(); - Logger.Log(LogLevel.INFO, "Server", "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Restart message sent via pipe at: {0}", DateTimeOffset.Now); } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, ex, "An error occurred during restart: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.ERROR, ex, "An error occurred during restart: {message}", ex.Message); } } public void OnPause() { - Logger.Log(LogLevel.INFO, "Server", "Pause command received at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Pause command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.SuspendPowerShellProcess(); _isPaused = true; - Logger.Log(LogLevel.INFO, "Server", "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); // Add delay to prevent rapid consecutive operations - Logger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); + PodePwshLogger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); Thread.Sleep(_delayMs); } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, ex, "Error occurred while suspending PowerShell process: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.ERROR, ex, "Error occurred while suspending PowerShell process: {message}", ex.Message); } } public void OnContinue() { - Logger.Log(LogLevel.INFO, "Server", "Continue command received at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Continue command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.ResumePowerShellProcess(); _isPaused = false; - Logger.Log(LogLevel.INFO, "Server", "Resume message sent via pipe at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Resume message sent via pipe at: {0}", DateTimeOffset.Now); // Add delay to prevent rapid consecutive operations - Logger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); + PodePwshLogger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); Thread.Sleep(_delayMs); } catch (Exception ex) { - Logger.Log(LogLevel.ERROR, ex, "Error occurred while resuming PowerShell process: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.ERROR, ex, "Error occurred while resuming PowerShell process: {message}", ex.Message); } } } diff --git a/src/PodePwshMonitor/Service/PodeWindowsService.cs b/src/PodePwshMonitor/Service/PodeWindowsService.cs index a9abf98c6..ce6f23827 100644 --- a/src/PodePwshMonitor/Service/PodeWindowsService.cs +++ b/src/PodePwshMonitor/Service/PodeWindowsService.cs @@ -23,15 +23,15 @@ public PodeWindowsService(IHost host, string serviceName ) protected override void OnStart(string[] args) { - Logger.Log(LogLevel.INFO,"Server","Service starting..."); + PodePwshLogger.Log(LogLevel.INFO,"Server","Service starting..."); try{ base.OnStart(args); _host.StartAsync().Wait(); - Logger.Log(LogLevel.INFO,"Server","Service started successfully.");} + PodePwshLogger.Log(LogLevel.INFO,"Server","Service started successfully.");} catch (Exception ex) { // Log the exception details to your custom log file - Logger.Log(LogLevel.ERROR,ex, "Service startup failed."); + PodePwshLogger.Log(LogLevel.ERROR,ex, "Service startup failed."); // Optionally write to the Windows Event Viewer for critical errors EventLog.WriteEntry(ServiceName, $"Critical failure during service startup: {ex.Message}\n{ex.StackTrace}", @@ -44,41 +44,41 @@ protected override void OnStart(string[] args) protected override void OnStop() { - Logger.Log(LogLevel.INFO,"Server","Service stopping..."); + PodePwshLogger.Log(LogLevel.INFO,"Server","Service stopping..."); base.OnStop(); _host.StopAsync().Wait(); - Logger.Log(LogLevel.INFO,"Server","Service stopped successfully."); + PodePwshLogger.Log(LogLevel.INFO,"Server","Service stopped successfully."); } protected override void OnPause() { - Logger.Log(LogLevel.INFO,"Server","Service pausing..."); + PodePwshLogger.Log(LogLevel.INFO,"Server","Service pausing..."); base.OnPause(); var service = _host.Services.GetService(typeof(IPausableHostedService)); if (service != null) { - Logger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodePwshLogger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnPause(); } else { - Logger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); + PodePwshLogger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); } } protected override void OnContinue() { - Logger.Log(LogLevel.INFO,"Server","Service resuming..."); + PodePwshLogger.Log(LogLevel.INFO,"Server","Service resuming..."); base.OnContinue(); var service = _host.Services.GetService(typeof(IPausableHostedService)); if (service != null) { - Logger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodePwshLogger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnContinue(); } else { - Logger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); + PodePwshLogger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); } } } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 9487becb5..0bb772881 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -60,7 +60,7 @@ function Start-PodeServiceHearthbeat { $scriptBlock = { while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - Write-PodeHost -Message "[Client] - Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" -Force + Write-PodeHost -Message "Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" -Force try { Start-Sleep -Milliseconds 100 # Create a named pipe server stream @@ -72,9 +72,9 @@ function Start-PodeServiceHearthbeat { [System.IO.Pipes.PipeOptions]::None ) - Write-PodeHost -Message "[Client] - Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." -Force + Write-PodeHost -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." -Force $pipeStream.WaitForConnection() # Wait until a client connects - Write-PodeHost -Message "[Client] - Connected to the $($PodeContext.Server.Service.PipeName) pipe." -Force + Write-PodeHost -Message "Connected to the $($PodeContext.Server.Service.PipeName) pipe." -Force # Create a StreamReader to read incoming messages from the pipe $reader = [System.IO.StreamReader]::new($pipeStream) @@ -86,26 +86,26 @@ function Start-PodeServiceHearthbeat { return } if ($message) { - Write-PodeHost -Message "[Client] - Received message: $message" -Force + Write-PodeHost -Message "Received message: $message" -Force switch ($message) { 'shutdown' { # Process 'shutdown' message - Write-PodeHost -Message '[Client] - Server requested shutdown. Closing client...' -Force + Write-PodeHost -Message 'Server requested shutdown. Closing client...' -Force Close-PodeServer # Gracefully stop Pode server return # Exit the loop } 'restart' { # Process 'restart' message - Write-PodeHost -Message '[Client] - Server requested restart. Restarting client...' -Force + Write-PodeHost -Message 'Server requested restart. Restarting client...' -Force Restart-PodeServer # Restart Pode server return # Exit the loop } 'suspend' { # Process 'suspend' message - Write-PodeHost -Message '[Client] - Server requested suspend. Suspending client...' -Force + Write-PodeHost -Message 'Server requested suspend. Suspending client...' -Force Start-Sleep 5 #Suspend-PodeServer # Suspend Pode server # return # Exit the loop @@ -113,7 +113,7 @@ function Start-PodeServiceHearthbeat { 'resume' { # Process 'resume' message - Write-PodeHost -Message '[Client] - Server requested resume. Resuming client...' -Force + Write-PodeHost -Message 'Server requested resume. Resuming client...' -Force Start-Sleep 5 #Resume-PodeServer # Resume Pode server # return # Exit the loop diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index a727429e6..66688c74b 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -512,15 +512,9 @@ function Stop-PodeService { Suspend-PodeService -Name 'MyService' .NOTES - This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. - -.EXCEPTION - Throws an exception if the service cannot be found, is not running, or if an error occurs while attempting to suspend the service. - -.SUPPORT - This function supports Windows only. + - This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. + - This function supports Windows only. #> - function Suspend-PodeService { param( [Parameter(Mandatory = $true)] @@ -587,13 +581,8 @@ function Suspend-PodeService { Resume-PodeService -Name 'MyService' .NOTES - This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. - -.EXCEPTION - Throws an exception if the service cannot be found, is not suspended, or if an error occurs while attempting to resume the service. - -.SUPPORT - This function supports Windows only. + - This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. + - This function supports Windows only. #> function Resume-PodeService { param( From 93ca6141555ad7150b32c166b68d5c5b50b015ba Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 17 Nov 2024 10:32:09 -0800 Subject: [PATCH 33/93] Added comments --- .../Service/IPausableHostedService.cs | 14 +- src/PodePwshMonitor/Service/PodePwshWorker.cs | 132 +++++++++++------- .../Service/PodeWindowsService.cs | 85 +++++++---- 3 files changed, 146 insertions(+), 85 deletions(-) diff --git a/src/PodePwshMonitor/Service/IPausableHostedService.cs b/src/PodePwshMonitor/Service/IPausableHostedService.cs index bf11b0c6c..5ef83b29e 100644 --- a/src/PodePwshMonitor/Service/IPausableHostedService.cs +++ b/src/PodePwshMonitor/Service/IPausableHostedService.cs @@ -1,8 +1,20 @@ namespace Pode.Service { + /// + /// 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(); } -} \ No newline at end of file +} diff --git a/src/PodePwshMonitor/Service/PodePwshWorker.cs b/src/PodePwshMonitor/Service/PodePwshWorker.cs index 9e0e52d80..02f6e3532 100644 --- a/src/PodePwshMonitor/Service/PodePwshWorker.cs +++ b/src/PodePwshMonitor/Service/PodePwshWorker.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.IO; using System.Threading; @@ -8,152 +7,177 @@ namespace Pode.Service { + /// + /// 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 PodePwshWorker : BackgroundService, IPausableHostedService { + // Logger instance for logging informational and error messages private readonly ILogger _logger; - private PodePwshMonitor _pwshMonitor; + // Instance of PodePwshMonitor to manage the Pode PowerShell process + private readonly PodePwshMonitor _pwshMonitor; + + // Tracks whether the worker is currently paused private volatile bool _isPaused; - private int _delayMs = 5000; // 5 seconds delay + // Delay in milliseconds to prevent rapid consecutive operations + private readonly int _delayMs = 5000; + /// + /// Initializes a new instance of the PodePwshWorker class. + /// + /// Logger instance for logging messages and errors. + /// Instance of PodePwshMonitor for managing the PowerShell process. public PodePwshWorker(ILogger logger, PodePwshMonitor pwshMonitor) { - _logger = logger; - _pwshMonitor = pwshMonitor; // Shared instance + _logger = logger; // Assign the logger + _pwshMonitor = pwshMonitor; // Assign the shared PodePwshMonitor instance _logger.LogInformation("PodePwshWorker initialized with shared PodePwshMonitor."); } - + /// + /// 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) { PodePwshLogger.Log(LogLevel.INFO, "Server", "PodePwshWorker running at: {0}", DateTimeOffset.Now); - - int retryCount = 0; + int retryCount = 0; // Tracks the number of retries in case of failures while (!stoppingToken.IsCancellationRequested) { if (_isPaused) { PodePwshLogger.Log(LogLevel.INFO, "Server", "Worker is paused. Waiting..."); - await Task.Delay(1000, stoppingToken); // Pause handling + await Task.Delay(1000, stoppingToken); // Wait while paused continue; } + try { - // Reset retry count on successful execution - retryCount = 0; + retryCount = 0; // Reset retry count on success - // Start the PowerShell process + // Start the Pode PowerShell process _pwshMonitor.StartPowerShellProcess(); - } catch (Exception ex) { retryCount++; - PodePwshLogger.Log(LogLevel.ERROR, ex, "An error occurred in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); + PodePwshLogger.Log(LogLevel.ERROR, ex, "Error in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); - // Check if maximum retries have been reached + // If retries exceed the maximum, log and exit the loop if (retryCount >= _pwshMonitor.StartMaxRetryCount) { - PodePwshLogger.Log(LogLevel.CRITICAL, "Maximum retry count reached. Breaking the monitoring loop."); - break; // Exit the loop + PodePwshLogger.Log(LogLevel.CRITICAL, "Maximum retry count reached. Exiting monitoring loop."); + break; } - // Wait for a while before retrying - try - { - await Task.Delay(_pwshMonitor.StartRetryDelayMs, stoppingToken); - } - catch (OperationCanceledException) - { - PodePwshLogger.Log(LogLevel.WARN, "Server", "Operation canceled during retry delay."); - break; // Exit the loop if the operation is canceled - } + // Delay before retrying + await Task.Delay(_pwshMonitor.StartRetryDelayMs, stoppingToken); } - // Wait before the next monitoring iteration + + // Add a delay between iterations await Task.Delay(10000, stoppingToken); } PodePwshLogger.Log(LogLevel.INFO, "Server", "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) { PodePwshLogger.Log(LogLevel.INFO, "Server", "Service is stopping at: {0}", DateTimeOffset.Now); + try { - _pwshMonitor.StopPowerShellProcess(); + _pwshMonitor.StopPowerShellProcess(); // Stop the PowerShell process } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error while stopping PowerShell process: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); } - // Wait for the base StopAsync to complete - await base.StopAsync(stoppingToken); - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopped successfully at: {0}", DateTimeOffset.Now); + await base.StopAsync(stoppingToken); // Wait for the base StopAsync to complete + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopped successfully at: {0}", DateTimeOffset.Now); } - // Custom RestartAsync method that sends a restart message via pipe + /// + /// Restarts the Pode PowerShell process by sending a restart command. + /// public void RestartAsync() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service is restarting at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service restarting at: {0}", DateTimeOffset.Now); + try { - // Send the 'restart' message using the pipe - _pwshMonitor.RestartPowerShellProcess(); - + _pwshMonitor.RestartPowerShellProcess(); // Restart the process PodePwshLogger.Log(LogLevel.INFO, "Server", "Restart message sent via pipe at: {0}", DateTimeOffset.Now); - } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "An error occurred during restart: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.ERROR, ex, "Error during restart: {0}", ex.Message); } } - + /// + /// Pauses the Pode PowerShell process and adds a delay to ensure stable operation. + /// public void OnPause() { PodePwshLogger.Log(LogLevel.INFO, "Server", "Pause command received at: {0}", DateTimeOffset.Now); try { - _pwshMonitor.SuspendPowerShellProcess(); - _isPaused = true; + _pwshMonitor.SuspendPowerShellProcess(); // Send pause command to the process + _isPaused = true; // Update the paused state + PodePwshLogger.Log(LogLevel.INFO, "Server", "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); - // Add delay to prevent rapid consecutive operations - PodePwshLogger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); - Thread.Sleep(_delayMs); + AddOperationDelay("Pause"); // Delay to ensure stability } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error occurred while suspending PowerShell process: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); } } + /// + /// Resumes the Pode PowerShell process and adds a delay to ensure stable operation. + /// public void OnContinue() { PodePwshLogger.Log(LogLevel.INFO, "Server", "Continue command received at: {0}", DateTimeOffset.Now); try { - _pwshMonitor.ResumePowerShellProcess(); - _isPaused = false; + _pwshMonitor.ResumePowerShellProcess(); // Send resume command to the process + _isPaused = false; // Update the paused state + PodePwshLogger.Log(LogLevel.INFO, "Server", "Resume message sent via pipe at: {0}", DateTimeOffset.Now); - // Add delay to prevent rapid consecutive operations - PodePwshLogger.Log(LogLevel.DEBUG, "Server", "Delaying for {0} ms to ensure stable operation.", _delayMs); - Thread.Sleep(_delayMs); + AddOperationDelay("Resume"); // Delay to ensure stability } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error occurred while resuming PowerShell process: {message}", ex.Message); + PodePwshLogger.Log(LogLevel.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) + { + PodePwshLogger.Log(LogLevel.DEBUG, "Server", "{0} operation completed. Adding delay of {1} ms.", operation, _delayMs); + Thread.Sleep(_delayMs); // Introduce a delay + } } } diff --git a/src/PodePwshMonitor/Service/PodeWindowsService.cs b/src/PodePwshMonitor/Service/PodeWindowsService.cs index ce6f23827..98daa1ce6 100644 --- a/src/PodePwshMonitor/Service/PodeWindowsService.cs +++ b/src/PodePwshMonitor/Service/PodeWindowsService.cs @@ -5,35 +5,47 @@ using System.Runtime.Versioning; using System.Diagnostics; - namespace Pode.Service { + /// + /// Represents a Windows service that integrates with a Pode host and supports lifecycle operations such as start, stop, pause, and continue. + /// [SupportedOSPlatform("windows")] public class PodeWindowsService : ServiceBase { - private readonly IHost _host; - + private readonly IHost _host; // The Pode host instance - public PodeWindowsService(IHost host, string serviceName ) + /// + /// Initializes a new instance of the PodeWindowsService class. + /// + /// The host instance managing the Pode application. + /// The name of the Windows service. + public PodeWindowsService(IHost host, string serviceName) { - _host = host; - CanPauseAndContinue = true; - ServiceName = serviceName; // Dynamically set the service name + _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) { - PodePwshLogger.Log(LogLevel.INFO,"Server","Service starting..."); - try{ - base.OnStart(args); - _host.StartAsync().Wait(); - PodePwshLogger.Log(LogLevel.INFO,"Server","Service started successfully.");} + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service starting..."); + try + { + base.OnStart(args); // Call the base implementation + _host.StartAsync().Wait(); // Start the Pode host asynchronously and wait for it to complete + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service started successfully."); + } catch (Exception ex) { - // Log the exception details to your custom log file - PodePwshLogger.Log(LogLevel.ERROR,ex, "Service startup failed."); + // Log the exception to the custom log + PodePwshLogger.Log(LogLevel.ERROR, ex, "Service startup failed."); - // Optionally write to the Windows Event Viewer for critical errors + // Write critical errors to the Windows Event Log EventLog.WriteEntry(ServiceName, $"Critical failure during service startup: {ex.Message}\n{ex.StackTrace}", EventLogEntryType.Error); @@ -42,45 +54,58 @@ protected override void OnStart(string[] args) } } + /// + /// Handles the service stop operation. Gracefully stops the Pode host. + /// protected override void OnStop() { - PodePwshLogger.Log(LogLevel.INFO,"Server","Service stopping..."); - base.OnStop(); - _host.StopAsync().Wait(); - PodePwshLogger.Log(LogLevel.INFO,"Server","Service stopped successfully."); + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopping..."); + base.OnStop(); // Call the base implementation + _host.StopAsync().Wait(); // Stop the Pode host asynchronously and wait for it to complete + PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopped successfully."); } + /// + /// Handles the service pause operation. Pauses the Pode host by invoking IPausableHostedService. + /// protected override void OnPause() { - PodePwshLogger.Log(LogLevel.INFO,"Server","Service pausing..."); - base.OnPause(); + PodePwshLogger.Log(LogLevel.INFO, "Server", "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) { - PodePwshLogger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); - ((IPausableHostedService)service).OnPause(); + PodePwshLogger.Log(LogLevel.DEBUG, "Server", $"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnPause(); // Invoke the pause operation } else { - PodePwshLogger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); + PodePwshLogger.Log(LogLevel.ERROR, "Server", "Error: Failed to resolve IPausableHostedService."); } } + /// + /// Handles the service resume operation. Resumes the Pode host by invoking IPausableHostedService. + /// protected override void OnContinue() { - PodePwshLogger.Log(LogLevel.INFO,"Server","Service resuming..."); - base.OnContinue(); + PodePwshLogger.Log(LogLevel.INFO, "Server", "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) { - PodePwshLogger.Log(LogLevel.DEBUG,"Server",$"Resolved IPausableHostedService: {service.GetType().FullName}"); - ((IPausableHostedService)service).OnContinue(); + PodePwshLogger.Log(LogLevel.DEBUG, "Server", $"Resolved IPausableHostedService: {service.GetType().FullName}"); + ((IPausableHostedService)service).OnContinue(); // Invoke the resume operation } else { - PodePwshLogger.Log(LogLevel.ERROR,"Server","Error:Failed to resolve IPausableHostedService."); + PodePwshLogger.Log(LogLevel.ERROR, "Server", "Error: Failed to resolve IPausableHostedService."); } } } } -#endif \ No newline at end of file +#endif From b04f761c515eec5d99df2fe0530d01739da356a7 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 18 Nov 2024 07:55:08 -0800 Subject: [PATCH 34/93] fixes --- Pode.sln | 2 +- pode.build.ps1 | 20 +- src/PodePwshMonitor/PodeMonitor.csproj | 18 +- src/PodePwshMonitor/Service/PodePwshMain.cs | 276 +++++++++++++----- src/PodePwshMonitor/Service/PodePwshWorker.cs | 2 +- .../Service/PodeWindowsService.cs | 2 - 6 files changed, 234 insertions(+), 86 deletions(-) diff --git a/Pode.sln b/Pode.sln index a1bd85799..913f48a35 100644 --- a/Pode.sln +++ b/Pode.sln @@ -7,7 +7,7 @@ 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\PodePwshMonitor\PodeMonitor.csproj", "{A927D6A5-A2AC-471A-9ABA-45916B597EB6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pode.Service", "src\PodePwshMonitor\PodeMonitor.csproj", "{A927D6A5-A2AC-471A-9ABA-45916B597EB6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/pode.build.ps1 b/pode.build.ps1 index d30a6d594..f3152bfce 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -113,7 +113,7 @@ function Invoke-PodeBuildDotnetBuild($target) { # Determine if the target framework is compatible $isCompatible = $False switch ($majorVersion) { - 9 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0','net9.0')) { $isCompatible = $True } } + 9 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0', 'net9.0')) { $isCompatible = $True } } 8 { if ($target -in @('net6.0', 'netstandard2.0', 'net8.0')) { $isCompatible = $True } } 7 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } 6 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } @@ -165,12 +165,20 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { else { $AssemblyVersion = '' } - foreach ($target in @('win-x64','win-arm64' ,'linux-x64','linux-arm64', 'osx-x64', 'osx-arm64')) { - $DefineConstants = '' - if ($target -like 'win-*') { - $DefineConstants = '-p:DefineConstants="WINDOWS"' + foreach ($target in @('win-x64', 'win-arm64' , 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')) { + $DefineConstants = @() + $ParamConstants = '' + + if (!$DisableSuspendSupport) { + $DefineConstants += 'ENABLE_LIFECYCLE_OPERATIONS' + } + + if ($DefineConstants.Count -gt 0) { + $ParamConstants = "-p:DefineConstants=`"$( $DefineConstants -join ';')`"" } - dotnet publish --runtime $target --output ../Bin/$target --configuration Release $AssemblyVersion "$DefineConstants" + + dotnet publish --runtime $target --output ../Bin/$target --configuration Release $AssemblyVersion $ParamConstants + if (!$?) { throw "dotnet publish failed for $($target)" } diff --git a/src/PodePwshMonitor/PodeMonitor.csproj b/src/PodePwshMonitor/PodeMonitor.csproj index f5ccbbef9..138614320 100644 --- a/src/PodePwshMonitor/PodeMonitor.csproj +++ b/src/PodePwshMonitor/PodeMonitor.csproj @@ -3,20 +3,24 @@ Exe net8.0 + true true - true - true - win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 + true + true + false + win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 + - - + - + + + - \ No newline at end of file + diff --git a/src/PodePwshMonitor/Service/PodePwshMain.cs b/src/PodePwshMonitor/Service/PodePwshMain.cs index a9109e6fa..50ccee975 100644 --- a/src/PodePwshMonitor/Service/PodePwshMain.cs +++ b/src/PodePwshMonitor/Service/PodePwshMain.cs @@ -5,105 +5,243 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; using System.Text.Json; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Diagnostics; - +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using Microsoft.Extensions.Logging; namespace Pode.Service { - public static class Program + /// + /// Entry point for the Pode service. Handles platform-specific configurations and signal-based operations. + /// + public static partial class Program { - public static void Main(string[] args) - { - var customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Custom config file from args or default + // Platform-dependent signal registration (for Linux/macOS) + [LibraryImport("libc", EntryPoint = "signal")] + private static partial int Signal(int signum, Action handler); - // Retrieve the service name from the configuration + private const int SIGSTOP = 19; // Signal for pause + private const int SIGCONT = 18; // Signal for continue + private const int SIGHUP = 1; // Signal for restart + private static PodePwshWorker _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"; - var config = new ConfigurationBuilder() + + // Load configuration + IConfigurationRoot config = new ConfigurationBuilder() .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) .Build(); - serviceName = config.GetSection("PodePwshWorker:Name").Value ?? serviceName; - PodePwshLogger.Initialize(config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log", LogLevel.INFO); - - var builder = Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((context, config) => - { - // Load configuration from the specified JSON file - config.AddJsonFile(customConfigFile, optional: false, reloadOnChange: true); - }) - .ConfigureServices((context, services) => - { - // Bind the PodePwshWorkerOptions section from configuration - services.Configure(context.Configuration.GetSection("PodePwshWorker")); - - // Add the PodePwshWorker as a hosted service - services.AddHostedService(); - -#if WINDOWS - // Register PodePwshMonitor as a singleton with proper error handling - services.AddSingleton(serviceProvider => - { - try - { - // Retrieve worker options from the service provider - var options = serviceProvider.GetRequiredService>().Value; - - // Log the options for debugging - PodePwshLogger.Log(LogLevel.INFO,"Server","Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); - - // Return the configured PodePwshMonitor instance - return new PodePwshMonitor(options); - } - catch (Exception ex) - { - // Log and write critical errors to the Event Log - PodePwshLogger.Log(LogLevel.ERROR,ex, "Failed to initialize PodePwshMonitor."); - - throw; // Rethrow to terminate the application - } - }); + serviceName = config.GetSection("PodePwshWorker:Name").Value ?? serviceName; + string logFilePath = config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log"; - // Register IPausableHostedService for handling pause and continue operations - services.AddSingleton(); -#endif - }); + // Initialize logger + PodePwshLogger.Initialize(logFilePath, LogLevel.INFO); + // Configure host builder + var builder = CreateHostBuilder(args, customConfigFile); - // Check if running on Linux and use Systemd + // Platform-specific logic if (OperatingSystem.IsLinux()) { - builder.UseSystemd(); - builder.Build().Run(); + ConfigureLinux(builder); } - // Check if running on Windows and use Windows Service else if (OperatingSystem.IsWindows()) { - //builder.UseWindowsService(); - // Windows-specific logic for CanPauseAndContinue -#if WINDOWS - using var host = builder.Build(); - var service = new PodeWindowsService(host,serviceName); - ServiceBase.Run(service); -#else - builder.UseWindowsService(); - builder.Build().Run(); -#endif + ConfigureWindows(builder, serviceName); } else if (OperatingSystem.IsMacOS()) { - // No specific macOS service manager, it runs under launchd - builder.Build().Run(); + ConfigureMacOS(builder); } else { - // Fallback for unsupported platforms PodePwshLogger.Log(LogLevel.WARN, "Server", "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("PodePwshWorker")); + + // Register PodePwshMonitor + services.AddSingleton(serviceProvider => + { + var options = serviceProvider.GetRequiredService>().Value; + PodePwshLogger.Log(LogLevel.INFO, "Server", "Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); + return new PodePwshMonitor(options); + }); + + // Register PodePwshWorker and track the instance + services.AddSingleton(provider => + { + var logger = provider.GetRequiredService>(); + var monitor = provider.GetRequiredService(); + var worker = new PodePwshWorker(logger, monitor); + _workerInstance = worker; // Track the instance globally + return worker; + }); + + // Add PodePwshWorker 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(SIGSTOP, _ => HandlePause()); + _ = Signal(SIGCONT, _ => HandleContinue()); + _ = Signal(SIGHUP, _ => HandleRestart()); + + 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(SIGSTOP, _ => HandlePause()); + _ = Signal(SIGCONT, _ => HandleContinue()); + _ = Signal(SIGHUP, _ => HandleRestart()); + + 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 PodeWindowsService(host, serviceName); + ServiceBase.Run(service); + + } + + /// + /// Handles the pause signal by pausing the Pode service. + /// + private static void HandlePause() + { + if (_workerInstance == null) + { + PodePwshLogger.Log(LogLevel.ERROR, "Server", "Pause requested, but _workerInstance is null."); return; } + PodePwshLogger.Log(LogLevel.INFO, "Server", "Pausing service..."); + _workerInstance?.OnPause(); + } + /// + /// Handles the continue signal by resuming the Pode service. + /// + private static void HandleContinue() + { + if (_workerInstance == null) + { + PodePwshLogger.Log(LogLevel.ERROR, "Server", "Continue requested, but _workerInstance is null."); + return; + } + PodePwshLogger.Log(LogLevel.INFO, "Server", "Resuming service..."); + _workerInstance?.OnContinue(); } + + /// + /// Handles the restart signal by restarting the Pode service. + /// + private static void HandleRestart() + { + if (_workerInstance == null) + { + PodePwshLogger.Log(LogLevel.ERROR, "Server", "Restart requested, but _workerInstance is null."); + return; + } + PodePwshLogger.Log(LogLevel.INFO, "Server", "Restarting service..."); + _workerInstance?.Restart(); + } + + /// + /// Performs cleanup operations before service termination. + /// + private static void Cleanup() + { + PodePwshLogger.Log(LogLevel.INFO, "Server", "Performing cleanup..."); + // Cleanup logic + } +#else + /// + /// Configures the Pode service for Linux, including signal handling. + /// + /// The host builder. + [SupportedOSPlatform("Linux")] + private static void ConfigureLinux(IHostBuilder builder) + { + 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) + { + builder.Build().Run(); + } + + /// + /// Configures the Pode service for Windows, enabling pause and continue support. + /// + /// The host builder. + /// The name of the service. + private static void ConfigureWindows(IHostBuilder builder, string serviceName) + { + builder.UseWindowsService(); + builder.Build().Run(); + } + +#endif } } diff --git a/src/PodePwshMonitor/Service/PodePwshWorker.cs b/src/PodePwshMonitor/Service/PodePwshWorker.cs index 02f6e3532..130f8d8e5 100644 --- a/src/PodePwshMonitor/Service/PodePwshWorker.cs +++ b/src/PodePwshMonitor/Service/PodePwshWorker.cs @@ -111,7 +111,7 @@ public override async Task StopAsync(CancellationToken stoppingToken) /// /// Restarts the Pode PowerShell process by sending a restart command. /// - public void RestartAsync() + public void Restart() { PodePwshLogger.Log(LogLevel.INFO, "Server", "Service restarting at: {0}", DateTimeOffset.Now); diff --git a/src/PodePwshMonitor/Service/PodeWindowsService.cs b/src/PodePwshMonitor/Service/PodeWindowsService.cs index 98daa1ce6..f63c9662f 100644 --- a/src/PodePwshMonitor/Service/PodeWindowsService.cs +++ b/src/PodePwshMonitor/Service/PodeWindowsService.cs @@ -1,4 +1,3 @@ -#if WINDOWS using System; using Microsoft.Extensions.Hosting; using System.ServiceProcess; @@ -108,4 +107,3 @@ protected override void OnContinue() } } } -#endif From d4ada0f7dd4996cb156fcf67abb134cf0cc90a18 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 18 Nov 2024 09:45:00 -0800 Subject: [PATCH 35/93] Add restart to windows using sc control 'Hello Service2' 128 --- examples/HelloService/HelloService.ps1 | 22 +- examples/HelloService/HelloServices.ps1 | 40 +++- src/Pode.psd1 | 3 +- .../Service/IPausableHostedService.cs | 4 + src/PodePwshMonitor/Service/PodePwshLogger.cs | 7 +- src/PodePwshMonitor/Service/PodePwshMain.cs | 18 +- .../Service/PodePwshMonitor.cs | 30 +-- src/PodePwshMonitor/Service/PodePwshWorker.cs | 26 +-- .../Service/PodeWindowsService.cs | 52 ++++- src/Private/Helpers.ps1 | 126 +++++++++- src/Private/Service.ps1 | 217 ++++++++++++------ src/Public/Metrics.ps1 | 87 ++++++- src/Public/Service.ps1 | 104 ++++++++- 13 files changed, 592 insertions(+), 144 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 07ddd2612..dba9107b9 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -31,6 +31,15 @@ .PARAMETER Query Queries the status of the 'Hello Service'. +.PARAMETER Suspend + Suspend the 'Hello Service'. + +.PARAMETER Resume + Resume the 'Hello Service'. + +.PARAMETER Restart + Restart the 'Hello Service'. + .EXAMPLE Register the service: ./HelloService.ps1 -Register @@ -95,7 +104,11 @@ param( [Parameter( ParameterSetName = 'Resume')] [switch] - $Resume + $Resume, + + [Parameter( ParameterSetName = 'Restart')] + [switch] + $Restart ) try { # Get the path of the script being executed @@ -152,9 +165,14 @@ if ($Query.IsPresent) { exit } +if ($Restart.IsPresent) { + Restart-PodeService -Name 'Hello Service2' + exit +} + # Start the Pode server Start-PodeServer { - New-PodeLoggingMethod -File -Name 'errors' -MaxDays 4 -Path './logs' | Enable-PodeErrorLogging + 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 diff --git a/examples/HelloService/HelloServices.ps1 b/examples/HelloService/HelloServices.ps1 index c534bb6c9..da6f8906a 100644 --- a/examples/HelloService/HelloServices.ps1 +++ b/examples/HelloService/HelloServices.ps1 @@ -26,6 +26,15 @@ .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 @@ -80,10 +89,22 @@ param( [Parameter( ParameterSetName = 'Stop')] [switch] $Stop, - + [Parameter( ParameterSetName = 'Query')] [switch] - $Query + $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 @@ -134,6 +155,21 @@ if ($Query.IsPresent) { exit } +if ($Resume.IsPresent) { + $services.GetEnumerator() | ForEach-Object { Resume-PodeService -Name $($_.Key) } + exit +} + +if ($Query.IsPresent) { + $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) } + exit +} + +if ($Restart.IsPresent) { + $services.GetEnumerator() | ForEach-Object { Restart-PodeService -Name $($_.Key) } + exit +} + # Start the Pode server Start-PodeServer { New-PodeLoggingMethod -File -Name "errors-$port" -MaxDays 4 -Path './logs' | Enable-PodeErrorLogging diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 8e4ceddde..936dc64af 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -507,7 +507,8 @@ 'Stop-PodeService', 'Get-PodeService', 'Suspend-PodeService', - 'Resume-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/PodePwshMonitor/Service/IPausableHostedService.cs b/src/PodePwshMonitor/Service/IPausableHostedService.cs index 5ef83b29e..2a27855a3 100644 --- a/src/PodePwshMonitor/Service/IPausableHostedService.cs +++ b/src/PodePwshMonitor/Service/IPausableHostedService.cs @@ -16,5 +16,9 @@ public interface IPausableHostedService /// This method is called when the service receives a continue command after being paused. /// void OnContinue(); + + + + void Restart(); } } diff --git a/src/PodePwshMonitor/Service/PodePwshLogger.cs b/src/PodePwshMonitor/Service/PodePwshLogger.cs index 569f64eee..7f92167e9 100644 --- a/src/PodePwshMonitor/Service/PodePwshLogger.cs +++ b/src/PodePwshMonitor/Service/PodePwshLogger.cs @@ -44,7 +44,7 @@ public static void Initialize(string filePath, LogLevel level) using (File.Create(logFilePath)) { } } - Log(LogLevel.INFO, "Server", "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", logFilePath, minLogLevel); + Log(LogLevel.INFO, "Server", Environment.ProcessId, "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", logFilePath, minLogLevel); } catch (Exception ex) { @@ -52,7 +52,7 @@ public static void Initialize(string filePath, LogLevel level) } } - public static void Log(LogLevel level, string context , string message = "", params object[] args) + public static void Log(LogLevel level, string context ,int pid, string message = "", params object[] args) { if (level < minLogLevel || string.IsNullOrEmpty(message)) { @@ -67,9 +67,6 @@ public static void Log(LogLevel level, string context , string message = "", par // Get the current time in ISO 8601 format in GMT/UTC string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); - // Get the current process ID - int pid = Environment.ProcessId; - // Build the log entry string logEntry = $"{timestamp} [PID:{pid}] [{level}] [{context}] {formattedMessage}"; diff --git a/src/PodePwshMonitor/Service/PodePwshMain.cs b/src/PodePwshMonitor/Service/PodePwshMain.cs index 50ccee975..91aa396b1 100644 --- a/src/PodePwshMonitor/Service/PodePwshMain.cs +++ b/src/PodePwshMonitor/Service/PodePwshMain.cs @@ -66,7 +66,7 @@ public static void Main(string[] args) } else { - PodePwshLogger.Log(LogLevel.WARN, "Server", "Unsupported platform. Exiting."); + PodePwshLogger.Log(LogLevel.WARN, "Server", Environment.ProcessId, "Unsupported platform. Exiting."); } } @@ -91,7 +91,7 @@ private static IHostBuilder CreateHostBuilder(string[] args, string customConfig services.AddSingleton(serviceProvider => { var options = serviceProvider.GetRequiredService>().Value; - PodePwshLogger.Log(LogLevel.INFO, "Server", "Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); return new PodePwshMonitor(options); }); @@ -166,10 +166,10 @@ private static void HandlePause() { if (_workerInstance == null) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", "Pause requested, but _workerInstance is null."); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Pause requested, but _workerInstance is null."); return; } - PodePwshLogger.Log(LogLevel.INFO, "Server", "Pausing service..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pausing service..."); _workerInstance?.OnPause(); } @@ -180,10 +180,10 @@ private static void HandleContinue() { if (_workerInstance == null) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", "Continue requested, but _workerInstance is null."); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Continue requested, but _workerInstance is null."); return; } - PodePwshLogger.Log(LogLevel.INFO, "Server", "Resuming service..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Resuming service..."); _workerInstance?.OnContinue(); } @@ -194,10 +194,10 @@ private static void HandleRestart() { if (_workerInstance == null) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", "Restart requested, but _workerInstance is null."); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Restart requested, but _workerInstance is null."); return; } - PodePwshLogger.Log(LogLevel.INFO, "Server", "Restarting service..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Restarting service..."); _workerInstance?.Restart(); } @@ -206,7 +206,7 @@ private static void HandleRestart() /// private static void Cleanup() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Performing cleanup..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Performing cleanup..."); // Cleanup logic } #else diff --git a/src/PodePwshMonitor/Service/PodePwshMonitor.cs b/src/PodePwshMonitor/Service/PodePwshMonitor.cs index 7f4d29bc9..c6cbb594c 100644 --- a/src/PodePwshMonitor/Service/PodePwshMonitor.cs +++ b/src/PodePwshMonitor/Service/PodePwshMonitor.cs @@ -48,7 +48,7 @@ public PodePwshMonitor(PodePwshWorkerOptions options) // Generate a unique pipe name for communication _pipeName = $"PodePipe_{Guid.NewGuid()}"; - PodePwshLogger.Log(LogLevel.INFO, "Server", $"Initialized PodePwshMonitor with pipe name: {_pipeName}"); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, $"Initialized PodePwshMonitor with pipe name: {_pipeName}"); } /// @@ -65,7 +65,7 @@ public void StartPowerShellProcess() // Log if the process is alive and log threshold is met if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process is Alive."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process is Alive."); _lastLogTime = DateTime.Now; } return; @@ -88,8 +88,8 @@ public void StartPowerShellProcess() }; // Subscribe to output and error streams - _powerShellProcess.OutputDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.INFO, "Pode", args.Data); - _powerShellProcess.ErrorDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.ERROR, "Pode", args.Data); + _powerShellProcess.OutputDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); + _powerShellProcess.ErrorDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); // Start the process _powerShellProcess.Start(); @@ -98,12 +98,12 @@ public void StartPowerShellProcess() // Log the process start time _lastLogTime = DateTime.Now; - PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process started successfully."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process started successfully."); } catch (Exception ex) { // Log any errors during process start - PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Failed to start Pode process: {ex.Message}"); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Failed to start Pode process: {ex.Message}"); PodePwshLogger.Log(LogLevel.DEBUG, ex); } } @@ -119,7 +119,7 @@ public void StopPowerShellProcess() { if (_powerShellProcess == null || _powerShellProcess.HasExited) { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process is not running."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process is not running."); return; } @@ -130,23 +130,23 @@ public void StopPowerShellProcess() // Send shutdown message and wait for process exit SendPipeMessage("shutdown"); - PodePwshLogger.Log(LogLevel.INFO, "Server", $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); // If process does not exit gracefully, forcefully terminate if (!_powerShellProcess.HasExited) { - PodePwshLogger.Log(LogLevel.WARN, "Server", "Pode process did not terminate gracefully, killing process."); + PodePwshLogger.Log(LogLevel.WARN, "Server", Environment.ProcessId, "Pode process did not terminate gracefully, killing process."); _powerShellProcess.Kill(); } - PodePwshLogger.Log(LogLevel.INFO, "Server", "Pode process stopped successfully."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process stopped successfully."); } } catch (Exception ex) { // Log errors during stop process - PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Error stopping Pode process: {ex.Message}"); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Error stopping Pode process: {ex.Message}"); PodePwshLogger.Log(LogLevel.DEBUG, ex); } finally @@ -194,13 +194,13 @@ private void ExecutePipeCommand(string command) if (InitializePipeClient()) // Ensure pipe client is initialized { SendPipeMessage(command); - PodePwshLogger.Log(LogLevel.INFO, "Server", $"{command.ToUpper()} command sent to Pode process."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, $"{command.ToUpper()} command sent to Pode process."); } } catch (Exception ex) { // Log errors during command execution - PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Error executing {command} command: {ex.Message}"); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Error executing {command} command: {ex.Message}"); PodePwshLogger.Log(LogLevel.DEBUG, ex); } finally @@ -234,7 +234,7 @@ private bool InitializePipeClient() if (!_pipeClient.IsConnected) { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Connecting to pipe server..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Connecting to pipe server..."); _pipeClient.Connect(10000); // Connect with a timeout of 10 seconds } @@ -254,7 +254,7 @@ private void SendPipeMessage(string message) } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", $"Error sending message to pipe: {ex.Message}"); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Error sending message to pipe: {ex.Message}"); PodePwshLogger.Log(LogLevel.DEBUG, ex); } } diff --git a/src/PodePwshMonitor/Service/PodePwshWorker.cs b/src/PodePwshMonitor/Service/PodePwshWorker.cs index 130f8d8e5..e88f14493 100644 --- a/src/PodePwshMonitor/Service/PodePwshWorker.cs +++ b/src/PodePwshMonitor/Service/PodePwshWorker.cs @@ -44,14 +44,14 @@ public PodePwshWorker(ILogger logger, PodePwshMonitor pwshMonito /// Cancellation token to signal when the worker should stop. protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - PodePwshLogger.Log(LogLevel.INFO, "Server", "PodePwshWorker running at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "PodePwshWorker running at: {0}", DateTimeOffset.Now); int retryCount = 0; // Tracks the number of retries in case of failures while (!stoppingToken.IsCancellationRequested) { if (_isPaused) { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Worker is paused. Waiting..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Worker is paused. Waiting..."); await Task.Delay(1000, stoppingToken); // Wait while paused continue; } @@ -71,7 +71,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // If retries exceed the maximum, log and exit the loop if (retryCount >= _pwshMonitor.StartMaxRetryCount) { - PodePwshLogger.Log(LogLevel.CRITICAL, "Maximum retry count reached. Exiting monitoring loop."); + PodePwshLogger.Log(LogLevel.CRITICAL, "Server", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); break; } @@ -83,7 +83,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Task.Delay(10000, stoppingToken); } - PodePwshLogger.Log(LogLevel.INFO, "Server", "Monitoring loop has stopped."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Monitoring loop has stopped."); } /// @@ -92,7 +92,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) /// Cancellation token to signal when the stop should occur. public override async Task StopAsync(CancellationToken stoppingToken) { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service is stopping at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); try { @@ -105,7 +105,7 @@ public override async Task StopAsync(CancellationToken stoppingToken) await base.StopAsync(stoppingToken); // Wait for the base StopAsync to complete - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopped successfully at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); } /// @@ -113,12 +113,12 @@ public override async Task StopAsync(CancellationToken stoppingToken) /// public void Restart() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service restarting at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); try { _pwshMonitor.RestartPowerShellProcess(); // Restart the process - PodePwshLogger.Log(LogLevel.INFO, "Server", "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); } catch (Exception ex) { @@ -131,14 +131,14 @@ public void Restart() /// public void OnPause() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Pause command received at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.SuspendPowerShellProcess(); // Send pause command to the process _isPaused = true; // Update the paused state - PodePwshLogger.Log(LogLevel.INFO, "Server", "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); AddOperationDelay("Pause"); // Delay to ensure stability } @@ -153,14 +153,14 @@ public void OnPause() /// public void OnContinue() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Continue command received at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.ResumePowerShellProcess(); // Send resume command to the process _isPaused = false; // Update the paused state - PodePwshLogger.Log(LogLevel.INFO, "Server", "Resume message sent via pipe at: {0}", DateTimeOffset.Now); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); AddOperationDelay("Resume"); // Delay to ensure stability } @@ -176,7 +176,7 @@ public void OnContinue() /// The name of the operation (e.g., "Pause" or "Resume"). private void AddOperationDelay(string operation) { - PodePwshLogger.Log(LogLevel.DEBUG, "Server", "{0} operation completed. Adding delay of {1} ms.", operation, _delayMs); + PodePwshLogger.Log(LogLevel.DEBUG, "Server", Environment.ProcessId, "{0} operation completed. Adding delay of {1} ms.", operation, _delayMs); Thread.Sleep(_delayMs); // Introduce a delay } } diff --git a/src/PodePwshMonitor/Service/PodeWindowsService.cs b/src/PodePwshMonitor/Service/PodeWindowsService.cs index f63c9662f..320a3939f 100644 --- a/src/PodePwshMonitor/Service/PodeWindowsService.cs +++ b/src/PodePwshMonitor/Service/PodeWindowsService.cs @@ -1,18 +1,21 @@ using System; -using Microsoft.Extensions.Hosting; +using System.IO; using System.ServiceProcess; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using System.Runtime.Versioning; using System.Diagnostics; namespace Pode.Service { /// - /// Represents a Windows service that integrates with a Pode host and supports lifecycle operations such as start, stop, pause, and continue. + /// 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 PodeWindowsService : 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 PodeWindowsService class. @@ -32,12 +35,12 @@ public PodeWindowsService(IHost host, string serviceName) /// Command-line arguments passed to the service. protected override void OnStart(string[] args) { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service starting..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", 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 - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service started successfully."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service started successfully."); } catch (Exception ex) { @@ -58,10 +61,10 @@ protected override void OnStart(string[] args) /// protected override void OnStop() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopping..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service stopping..."); base.OnStop(); // Call the base implementation _host.StopAsync().Wait(); // Stop the Pode host asynchronously and wait for it to complete - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service stopped successfully."); + PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service stopped successfully."); } /// @@ -69,19 +72,19 @@ protected override void OnStop() /// protected override void OnPause() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service pausing..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", 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) { - PodePwshLogger.Log(LogLevel.DEBUG, "Server", $"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodePwshLogger.Log(LogLevel.DEBUG, "Server", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnPause(); // Invoke the pause operation } else { - PodePwshLogger.Log(LogLevel.ERROR, "Server", "Error: Failed to resolve IPausableHostedService."); + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); } } @@ -90,19 +93,44 @@ protected override void OnPause() /// protected override void OnContinue() { - PodePwshLogger.Log(LogLevel.INFO, "Server", "Service resuming..."); + PodePwshLogger.Log(LogLevel.INFO, "Server", 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) { - PodePwshLogger.Log(LogLevel.DEBUG, "Server", $"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodePwshLogger.Log(LogLevel.DEBUG, "Server", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnContinue(); // Invoke the resume operation } else { - PodePwshLogger.Log(LogLevel.ERROR, "Server", "Error: Failed to resolve IPausableHostedService."); + PodePwshLogger.Log(LogLevel.ERROR, "Server", 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) + { + PodePwshLogger.Log(LogLevel.INFO, "Server", 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 + { + PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService for restart."); + } + } + else + { + base.OnCustomCommand(command); // Handle other custom commands } } } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index fc16ff38f..831ab119e 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3723,10 +3723,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 ( @@ -3746,6 +3746,7 @@ function Copy-PodeObjectDeepClone { return [System.Management.Automation.PSSerializer]::Deserialize($xmlSerializer) } } + <# .SYNOPSIS Tests if the current user has administrative privileges on Windows or root/sudo privileges on Linux/macOS. @@ -3786,9 +3787,10 @@ function Copy-PodeObjectDeepClone { 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 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 { @@ -3851,6 +3853,7 @@ function Test-PodeAdminPrivilege { return $false } } + <# .SYNOPSIS Starts a command with elevated privileges if the current session is not already elevated. @@ -3877,7 +3880,7 @@ function Test-PodeAdminPrivilege { This will run the script `MyScript.ps1` with elevated privileges, pass the parameters `-Param1` and `-Param2`, and return the result. .NOTES - This function is particularly useful when running commands or scripts that require administrator rights. + This is an internal function and may change in future releases of Pode. #> function Invoke-PodeWinElevatedCommand { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '')] @@ -3916,3 +3919,110 @@ function Invoke-PodeWinElevatedCommand { return Invoke-Expression "$Command $Arguments" } +<# +.SYNOPSIS + Converts a duration in milliseconds into a human-readable time format. + +.DESCRIPTION + This function takes an input duration in milliseconds and converts it into + a readable time format. It supports multiple output styles, such as verbose + (detailed text), compact (`dd:hh:mm:ss`), and concise (short notation). + Optionally, milliseconds can be excluded from the output. + +.PARAMETER Milliseconds + The duration in milliseconds to be converted. + +.PARAMETER VerboseOutput + If specified, outputs a detailed, descriptive format (e.g., "1 day, 2 hours, 3 minutes"). + +.PARAMETER CompactOutput + If specified, outputs a compact format (e.g., "dd:hh:mm:ss"). + +.PARAMETER ExcludeMilliseconds + If specified, excludes milliseconds from the output. + +.EXAMPLE + Convert-PodeMillisecondsToReadable -Milliseconds 123456789 + + Output: + 1d 10h 17m 36s + +.EXAMPLE + Convert-PodeMillisecondsToReadable -Milliseconds 123456789 -VerboseOutput + + Output: + 1 day, 10 hours, 17 minutes, 36 seconds, 789 milliseconds + +.EXAMPLE + Convert-PodeMillisecondsToReadable -Milliseconds 123456789 -CompactOutput -ExcludeMilliseconds + + Output: + 01:10:17:36 + +.NOTES + This is an internal function and may change in future releases of Pode. +#> + +function Convert-PodeMillisecondsToReadable { + param ( + [Parameter(Mandatory)] + [long]$Milliseconds, + + [switch]$VerboseOutput, # Provide detailed descriptions + [switch]$CompactOutput, # Provide compact format like dd:hh:mm:ss or mm:ss:ms + [switch]$ExcludeMilliseconds # Exclude milliseconds from the output + ) + + $timeSpan = [timespan]::FromMilliseconds($Milliseconds) + + if ($CompactOutput) { + # Dynamically build compact format + $components = @() + + # Include days only if greater than 0 + if ($timeSpan.Days -gt 0) { $components += '{0:D2}' -f $timeSpan.Days } + + # Include hours only if greater than 0 or days are included + if ($timeSpan.Hours -gt 0 -or $components.Count -gt 0) { $components += '{0:D2}' -f $timeSpan.Hours } + + # Include minutes if relevant + if ($timeSpan.Minutes -gt 0 -or $components.Count -gt 0) { $components += '{0:D2}' -f $timeSpan.Minutes } + + # Add seconds as the final required time component + $components += '{0:D2}' -f $timeSpan.Seconds + + # Append milliseconds if not excluded + if (-not $ExcludeMilliseconds) { + $components[-1] += ':{0:D3}' -f $timeSpan.Milliseconds + } + + # Join with ":" and return + return $components -join ':' + } + + # Default or verbose format + if ($VerboseOutput) { + $verboseParts = @() + if ($timeSpan.Days -gt 0) { $verboseParts += "$($timeSpan.Days) day$(if ($timeSpan.Days -ne 1) { 's' })" } + if ($timeSpan.Hours -gt 0) { $verboseParts += "$($timeSpan.Hours) hour$(if ($timeSpan.Hours -ne 1) { 's' })" } + if ($timeSpan.Minutes -gt 0) { $verboseParts += "$($timeSpan.Minutes) minute$(if ($timeSpan.Minutes -ne 1) { 's' })" } + if ($timeSpan.Seconds -gt 0) { $verboseParts += "$($timeSpan.Seconds) second$(if ($timeSpan.Seconds -ne 1) { 's' })" } + if (-not $ExcludeMilliseconds -and $timeSpan.Milliseconds -gt 0) { + $verboseParts += "$($timeSpan.Milliseconds) millisecond$(if ($timeSpan.Milliseconds -ne 1) { 's' })" + } + + return $verboseParts -join ' ' + } + + # Default concise format + $parts = @() + if ($timeSpan.Days -gt 0) { $parts += "$($timeSpan.Days)d" } + if ($timeSpan.Hours -gt 0 -or $parts.Count -gt 0) { $parts += "$($timeSpan.Hours)h" } + if ($timeSpan.Minutes -gt 0 -or $parts.Count -gt 0) { $parts += "$($timeSpan.Minutes)m" } + if ($timeSpan.Seconds -gt 0 -or $parts.Count -gt 0) { $parts += "$($timeSpan.Seconds)s" } + if (-not $ExcludeMilliseconds -and $timeSpan.Milliseconds -gt 0 -or $parts.Count -gt 0) { + $parts += "$($timeSpan.Milliseconds)ms" + } + + return $parts -join ':' +} diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 0bb772881..fa0cb06de 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -61,6 +61,9 @@ function Start-PodeServiceHearthbeat { while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { Write-PodeHost -Message "Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" -Force + Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force + Write-PodeHost -Message "Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force + Write-PodeHost -Message "Total Number of Restart: $(Get-PodeServerRestartCount)" -Force try { Start-Sleep -Milliseconds 100 # Create a named pipe server stream @@ -91,32 +94,32 @@ function Start-PodeServiceHearthbeat { switch ($message) { 'shutdown' { # Process 'shutdown' message - Write-PodeHost -Message 'Server requested shutdown. Closing client...' -Force + Write-PodeHost -Message 'Server requested shutdown. Closing Pode ...' -Force Close-PodeServer # Gracefully stop Pode server return # Exit the loop } 'restart' { # Process 'restart' message - Write-PodeHost -Message 'Server requested restart. Restarting client...' -Force + Write-PodeHost -Message 'Server requested restart. Restarting Pode ...' -Force Restart-PodeServer # Restart Pode server return # Exit the loop } 'suspend' { # Process 'suspend' message - Write-PodeHost -Message 'Server requested suspend. Suspending client...' -Force + Write-PodeHost -Message 'Server requested suspend. Suspending Pode ...' -Force Start-Sleep 5 #Suspend-PodeServer # Suspend Pode server - # return # Exit the loop + # return # Exit the loop } 'resume' { # Process 'resume' message - Write-PodeHost -Message 'Server requested resume. Resuming client...' -Force + Write-PodeHost -Message 'Server requested resume. Resuming Pode ...' -Force Start-Sleep 5 #Resume-PodeServer # Resume Pode server - # return # Exit the loop + # return # Exit the loop } } @@ -394,7 +397,10 @@ ExecStart=$execStart WorkingDirectory=$BinPath Restart=always User=$User -#Group=$Group +KillMode=process +Environment=NOTIFY_SOCKET=/run/systemd/notify +# Uncomment and adjust if needed +# Group=$Group # Environment=DOTNET_CLI_TELEMETRY_OPTOUT=1 # Environment=ASPNETCORE_ENVIRONMENT=Production @@ -620,21 +626,21 @@ function Confirm-PodeAdminPrivilege { } <# - .SYNOPSIS +.SYNOPSIS Tests if a Linux service is registered. - .DESCRIPTION +.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 +.PARAMETER Name The name of the Linux service to test. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is registered; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Test-PodeLinuxServiceIsRegistered { @@ -648,21 +654,21 @@ function Test-PodeLinuxServiceIsRegistered { } <# - .SYNOPSIS +.SYNOPSIS Tests if a Linux service is active. - .DESCRIPTION +.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 +.PARAMETER Name The name of the Linux service to check. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is active; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Test-PodeLinuxServiceIsActive { @@ -676,21 +682,21 @@ function Test-PodeLinuxServiceIsActive { } <# - .SYNOPSIS +.SYNOPSIS Disables a Linux service. - .DESCRIPTION +.DESCRIPTION Disables a specified Linux service by using the `sudo systemctl disable` command. It returns `$true` if the service is successfully disabled. - .PARAMETER Name +.PARAMETER Name The name of the Linux service to disable. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is successfully disabled; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Disable-PodeLinuxService { @@ -704,21 +710,21 @@ function Disable-PodeLinuxService { } <# - .SYNOPSIS +.SYNOPSIS Enables a Linux service. - .DESCRIPTION +.DESCRIPTION Enables a specified Linux service by using the `sudo systemctl enable` command. It returns `$true` if the service is successfully enabled. - .PARAMETER Name +.PARAMETER Name The name of the Linux service to enable. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is successfully enabled; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Enable-PodeLinuxService { @@ -732,21 +738,21 @@ function Enable-PodeLinuxService { } <# - .SYNOPSIS +.SYNOPSIS Stops a Linux service. - .DESCRIPTION +.DESCRIPTION Stops a specified Linux service by using the `systemctl stop` command. It returns `$true` if the service is successfully stopped. - .PARAMETER Name +.PARAMETER Name The name of the Linux service to stop. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is successfully stopped; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Stop-PodeLinuxService { @@ -760,21 +766,21 @@ function Stop-PodeLinuxService { } <# - .SYNOPSIS +.SYNOPSIS Starts a Linux service. - .DESCRIPTION +.DESCRIPTION Starts a specified Linux service by using the `systemctl start` command. It returns `$true` if the service is successfully started. - .PARAMETER Name +.PARAMETER Name The name of the Linux service to start. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is successfully started; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Start-PodeLinuxService { @@ -788,21 +794,21 @@ function Start-PodeLinuxService { } <# - .SYNOPSIS +.SYNOPSIS Tests if a macOS service is registered. - .DESCRIPTION +.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 +.PARAMETER Name The name of the macOS service to test. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is registered; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Test-PodeMacOsServiceIsRegistered { @@ -816,21 +822,98 @@ function Test-PodeMacOsServiceIsRegistered { } <# - .SYNOPSIS +.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( + $Name + ) + if ($IsLinux) { + return Test-PodeLinuxServiceIsRegistered + } + if ($IsMacOS) { + return Test-PodeMacOsServiceIsRegistered + } + if ($IsWindows) { + $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" + return $null -eq $service + } +} + +<# +.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( + $Name + ) + if ($IsLinux) { + return Test-PodeLinuxServiceIsActive + } + if ($IsMacOS) { + return Test-PodeMacOsServiceIsActive + } + if ($IsWindows) { + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service) { + # Check if the service is already running + return ($service.Status -ne 'Running') + } + return $false + } +} + + +<# +.SYNOPSIS Tests if a macOS service is active. - .DESCRIPTION +.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 +.PARAMETER Name The name of the macOS service to check. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is active; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Test-PodeMacOsServiceIsActive { @@ -844,21 +927,21 @@ function Test-PodeMacOsServiceIsActive { } <# - .SYNOPSIS +.SYNOPSIS Retrieves the PID of a macOS service. - .DESCRIPTION +.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 +PARAMETER Name The name of the macOS service whose PID you want to retrieve. - .OUTPUTS +.OUTPUTS [int] Returns the PID of the service if it is active; otherwise, returns `0`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Get-PodeMacOsServicePid { @@ -872,21 +955,21 @@ function Get-PodeMacOsServicePid { } <# - .SYNOPSIS +.SYNOPSIS Disables a macOS service. - .DESCRIPTION +.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 +.PARAMETER Name The name of the macOS service to disable. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is successfully disabled; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Disable-PodeMacOsService { @@ -900,21 +983,21 @@ function Disable-PodeMacOsService { } <# - .SYNOPSIS +.SYNOPSIS Stops a macOS service. - .DESCRIPTION +.DESCRIPTION Stops a specified macOS service by using the `launchctl stop` command. It returns `$true` if the service is successfully stopped. - .PARAMETER Name +.PARAMETER Name The name of the macOS service to stop. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is successfully stopped; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Stop-PodeMacOsService { @@ -928,21 +1011,21 @@ function Stop-PodeMacOsService { } <# - .SYNOPSIS +.SYNOPSIS Starts a macOS service. - .DESCRIPTION +.DESCRIPTION Starts a specified macOS service by using the `launchctl start` command. It returns `$true` if the service is successfully started. - .PARAMETER Name +.PARAMETER Name The name of the macOS service to start. - .OUTPUTS +.OUTPUTS [bool] Returns `$true` if the service is successfully started; otherwise, `$false`. - .NOTES +.NOTES This is an internal function and may change in future releases of Pode. #> function Start-PodeMacOsService { diff --git a/src/Public/Metrics.ps1 b/src/Public/Metrics.ps1 index 0d47243d4..2d42b13e4 100644 --- a/src/Public/Metrics.ps1 +++ b/src/Public/Metrics.ps1 @@ -1,35 +1,104 @@ <# .SYNOPSIS -Returns the uptime of the server in milliseconds. + Returns the uptime of the server in milliseconds or in a human-readable format. .DESCRIPTION -Returns the uptime of the server in milliseconds. You can optionally return the total uptime regardless of server restarts. + Returns the uptime of the server in milliseconds by default. You can optionally return the total uptime regardless of server restarts or convert the uptime to a human-readable format with selectable output styles (e.g., Verbose, Compact). + Additionally, milliseconds can be excluded from the output if desired. .PARAMETER Total -If supplied, the total uptime of the server will be returned, regardless of restarts. + If supplied, the total uptime of the server will be returned, regardless of restarts. + +.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) + +.EXAMPLE + $totalUptime = Get-PodeServerUptime -Total + # Output: 987654321 (milliseconds) .EXAMPLE -$currentUptime = Get-PodeServerUptime + $readableUptime = Get-PodeServerUptime -Readable + # Output: "1d 10h 17m 36s" .EXAMPLE -$totalUptime = Get-PodeServerUptime -Total + $verboseUptime = Get-PodeServerUptime -Readable -OutputType Verbose + # Output: "1 day, 10 hours, 17 minutes, 36 seconds, 789 milliseconds" + +.EXAMPLE + $compactUptime = Get-PodeServerUptime -Readable -OutputType Compact + # Output: "01:10:17:36" + +.EXAMPLE + $compactUptimeNoMs = Get-PodeServerUptime -Readable -OutputType Compact -ExcludeMilliseconds + # Output: "01:10:17:36" #> function Get-PodeServerUptime { - [CmdletBinding()] - [OutputType([long])] + [CmdletBinding(DefaultParameterSetName = 'Milliseconds')] + [OutputType([long], [string])] param( + # Common to all parameter sets [switch] - $Total + $Total, + + # Default set: Milliseconds output + [Parameter(ParameterSetName = 'Readable')] + [switch] + $Readable, + + # Available only when -Readable is specified + [Parameter(ParameterSetName = 'Readable')] + [ValidateSet("Verbose", "Compact", "Default")] + [string] + $OutputType = "Default", + + # Available only when -Readable is specified + [Parameter(ParameterSetName = 'Readable')] + [switch] + $ExcludeMilliseconds ) + # Determine the appropriate start time $time = $PodeContext.Metrics.Server.StartTime if ($Total) { $time = $PodeContext.Metrics.Server.InitialLoadTime } - return [long]([datetime]::UtcNow - $time).TotalMilliseconds + # Calculate uptime in milliseconds + $uptimeMilliseconds = [long]([datetime]::UtcNow - $time).TotalMilliseconds + + # Handle readable output + if ($PSCmdlet.ParameterSetName -eq 'Readable') { + switch ($OutputType) { + "Verbose" { + return Convert-PodeMillisecondsToReadable -Milliseconds $uptimeMilliseconds -VerboseOutput -ExcludeMilliseconds:$ExcludeMilliseconds + } + "Compact" { + return Convert-PodeMillisecondsToReadable -Milliseconds $uptimeMilliseconds -CompactOutput -ExcludeMilliseconds:$ExcludeMilliseconds + } + "Default" { + return Convert-PodeMillisecondsToReadable -Milliseconds $uptimeMilliseconds -ExcludeMilliseconds:$ExcludeMilliseconds + } + } + } + + # Default to milliseconds if no readable output is requested + return $uptimeMilliseconds } + <# .SYNOPSIS Returns the number of times the server has restarted. diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 66688c74b..c82917287 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -1040,4 +1040,106 @@ function Get-PodeService { return $null } } -} \ No newline at end of file +} + +<# +.SYNOPSIS + Restarts a Pode service on Windows, Linux, or macOS by sending the appropriate restart signal. + +.DESCRIPTION + This function handles the restart operation for a Pode service across multiple platforms: + - On Windows: Sends a restart control signal (128) using `sc control`. + - On Linux and macOS: Sends the `SIGHUP` signal to the service's process ID. + +.PARAMETER Name + The name of the Pode service to restart. + +.NOTES + Requires administrative/root privileges to execute service operations. + + This function leverages platform-specific methods: + - Windows: Uses `sc control` for service control commands. + - Linux/macOS: Uses `/bin/kill -SIGHUP` to signal the service's process. + + For services not running, a verbose message is displayed, and no restart signal is sent. + +.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. + +#> +function Restart-PodeService { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + try { + # Ensure the script is running with the necessary administrative/root privileges. + # Exits the script if the current user lacks the required privileges. + Confirm-PodeAdminPrivilege + + Write-Verbose -Message "Attempting to restart service '$Name' on platform $([System.Environment]::OSVersion.Platform)..." + + if ($IsWindows) { + # Handle Windows-specific restart logic + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if ($service) { + if ($service.Status -eq 'Running' -or $service.Status -eq 'Paused') { + Write-Verbose -Message "Sending restart (128) signal to service '$Name'." + $null = Invoke-PodeWinElevatedCommand -Command 'sc control' -Arguments "'$Name' 128" + $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + Write-Verbose -Message "Service '$Name' restart signal sent successfully." + } + else { + Write-Verbose -Message "Service '$Name' is not running." + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + } + elseif ($IsLinux -or $IsMacOS) { + # Standardize service naming for Linux/macOS + $nameService = $(if ($IsMacOS) { "pode.$Name.service".Replace(' ', '_') }else { "$Name.service".Replace(' ', '_') }) + # Check if the service is registered + if ((Test-PodeServiceIsRegistered -Name $nameService)) { + if ((Test-PodeServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' is active. Sending SIGHUP signal." + $svc = Get-PodeService -Name $nameService + sudo /bin/kill -SIGHUP $svc.ProcessId + Write-Verbose -Message "SIGHUP signal sent to service '$Name'." + } + else { + Write-Verbose -Message "Service '$Name' is not running." + } + } + else { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) + } + } + else { + # Unsupported platform + Write-Error -Message "Unsupported platform. Unable to restart service '$Name'." + return $false + } + } + 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 +} From ab64da5cce60f5bca01a75f3cd482b557bf859cb Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 18 Nov 2024 09:51:36 -0800 Subject: [PATCH 36/93] add EnableTransactions to mac plist --- src/Private/Service.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index fa0cb06de..36a03eb11 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -259,6 +259,10 @@ function Register-PodeMacService { SuccessfulExit + + + EnableTransactions + "@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -Encoding UTF8 @@ -399,9 +403,9 @@ 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=DOTNET_CLI_TELEMETRY_OPTOUT=1 # Environment=ASPNETCORE_ENVIRONMENT=Production [Install] @@ -556,8 +560,6 @@ function Register-PodeWindowsService { $sv = Invoke-PodeWinElevatedCommand -Command 'New-Service' -Arguments ($paramsString -join ' ') -Credential $Credential - - if (!$sv) { # Service registration failed. throw ($PodeLocale.serviceRegistrationException -f "$Name") From 0e5765dcb72aa973715dd0816428a7a82856b3d0 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 18 Nov 2024 18:04:40 -0800 Subject: [PATCH 37/93] Rename folders and fix windows service credential --- Pode.sln | 2 +- examples/HelloService/HelloService.ps1 | 42 +++++++----- examples/HelloService/HelloServices.ps1 | 9 ++- examples/HelloWorld/servicesettings.json | 4 +- pode.build.ps1 | 2 +- .../IPausableHostedService.cs | 2 +- .../PodeMonitor.cs} | 50 +++++++------- .../PodeMonitor.csproj | 0 .../PodeMonitorLogger.cs} | 10 +-- .../PodeMonitorMain.cs} | 56 ++++++++-------- .../PodeMonitorWindowsService.cs} | 34 +++++----- .../PodeMonitorWorker.cs} | 66 ++++++++----------- .../PodeMonitorWorkerOptions.cs} | 6 +- src/Private/Helpers.ps1 | 9 ++- src/Private/Service.ps1 | 10 +-- src/Public/Service.ps1 | 18 ++++- 16 files changed, 171 insertions(+), 149 deletions(-) rename src/{PodePwshMonitor/Service => PodeMonitor}/IPausableHostedService.cs (96%) rename src/{PodePwshMonitor/Service/PodePwshMonitor.cs => PodeMonitor/PodeMonitor.cs} (79%) rename src/{PodePwshMonitor => PodeMonitor}/PodeMonitor.csproj (100%) rename src/{PodePwshMonitor/Service/PodePwshLogger.cs => PodeMonitor/PodeMonitorLogger.cs} (94%) rename src/{PodePwshMonitor/Service/PodePwshMain.cs => PodeMonitor/PodeMonitorMain.cs} (75%) rename src/{PodePwshMonitor/Service/PodeWindowsService.cs => PodeMonitor/PodeMonitorWindowsService.cs} (70%) rename src/{PodePwshMonitor/Service/PodePwshWorker.cs => PodeMonitor/PodeMonitorWorker.cs} (57%) rename src/{PodePwshMonitor/Service/PodePwshWorkerOptions.cs => PodeMonitor/PodeMonitorWorkerOptions.cs} (95%) diff --git a/Pode.sln b/Pode.sln index 913f48a35..02438e28a 100644 --- a/Pode.sln +++ b/Pode.sln @@ -7,7 +7,7 @@ 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}") = "Pode.Service", "src\PodePwshMonitor\PodeMonitor.csproj", "{A927D6A5-A2AC-471A-9ABA-45916B597EB6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PodeMonitor", "src\PodeMonitor\PodeMonitor.csproj", "{A927D6A5-A2AC-471A-9ABA-45916B597EB6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index dba9107b9..3f7212b51 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -3,7 +3,7 @@ PowerShell script to register, start, stop, query, and unregister a Pode service, with a basic server setup. .DESCRIPTION - This script manages a Pode service named 'Hello Service' with commands to register, start, stop, query, + This script manages a Pode service named 'Hello Service3' with commands to register, start, stop, query, and unregister the service. Additionally, it sets up a Pode server that listens on port 8080 and includes a simple GET route that responds with 'Hello, Service!'. @@ -14,31 +14,34 @@ # Response: 'Hello, Service!' .PARAMETER Register - Registers the 'Hello Service' with Pode. + Registers the 'Hello Service3' with Pode. + +.PARAMETER Password + A secure password for the service account (Windows only). If omitted, the service account will be 'NT AUTHORITY\SYSTEM'. .PARAMETER Unregister - Unregisters the 'Hello Service' from Pode. Use with the -Force switch to forcefully unregister the service. + Unregisters the 'Hello Service3' 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 'Hello Service'. + Starts the 'Hello Service3'. .PARAMETER Stop - Stops the 'Hello Service'. + Stops the 'Hello Service3'. .PARAMETER Query - Queries the status of the 'Hello Service'. + Queries the status of the 'Hello Service3'. .PARAMETER Suspend - Suspend the 'Hello Service'. + Suspend the 'Hello Service3'. .PARAMETER Resume - Resume the 'Hello Service'. + Resume the 'Hello Service3'. .PARAMETER Restart - Restart the 'Hello Service'. + Restart the 'Hello Service3'. .EXAMPLE Register the service: @@ -78,6 +81,10 @@ param( [switch] $Register, + [Parameter(Mandatory = $false, ParameterSetName = 'Register', ValueFromPipeline = $true )] + [securestring] + $Password, + [Parameter(Mandatory = $true, ParameterSetName = 'Unregister')] [switch] $Unregister, @@ -109,6 +116,7 @@ param( [Parameter( ParameterSetName = 'Restart')] [switch] $Restart + ) try { # Get the path of the script being executed @@ -133,40 +141,40 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service2' -ParameterString "-Port $Port" # -Password (ConvertTo-SecureString 'Pata2Pata1' -AsPlainText -Force) + Register-PodeService -Name 'Hello Service3' -ParameterString "-Port $Port" -Password $Password exit } if ( $Unregister.IsPresent) { - Unregister-PodeService -Name 'Hello Service2' -Force:$Force + Unregister-PodeService -Name 'Hello Service3' -Force:$Force exit } if ($Start.IsPresent) { - Start-PodeService -Name 'Hello Service2' + Start-PodeService -Name 'Hello Service3' exit } if ($Stop.IsPresent) { - Stop-PodeService -Name 'Hello Service2' + Stop-PodeService -Name 'Hello Service3' exit } if ($Suspend.IsPresent) { - Suspend-PodeService -Name 'Hello Service2' + Suspend-PodeService -Name 'Hello Service3' exit } if ($Resume.IsPresent) { - Resume-PodeService -Name 'Hello Service2' + Resume-PodeService -Name 'Hello Service3' exit } if ($Query.IsPresent) { - Get-PodeService -Name 'Hello Service2' + Get-PodeService -Name 'Hello Service3' exit } if ($Restart.IsPresent) { - Restart-PodeService -Name 'Hello Service2' + Restart-PodeService -Name 'Hello Service3' exit } diff --git a/examples/HelloService/HelloServices.ps1 b/examples/HelloService/HelloServices.ps1 index da6f8906a..5a9e72383 100644 --- a/examples/HelloService/HelloServices.ps1 +++ b/examples/HelloService/HelloServices.ps1 @@ -11,6 +11,9 @@ .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 Unregister Unregisters all services specified in the hashtable. Use with -Force to force unregistration. @@ -74,6 +77,10 @@ param( [switch] $Register, + [Parameter(Mandatory = $false, ParameterSetName = 'Register', ValueFromPipeline = $true )] + [securestring] + $Password, + [Parameter(Mandatory = $true, ParameterSetName = 'Unregister')] [switch] $Unregister, @@ -133,7 +140,7 @@ $services=@{ } if ( $Register.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Register-PodeService -Name $($_.Key) -ParameterString "-Port $($_.Value)" } + $services.GetEnumerator() | ForEach-Object { Register-PodeService -Name $($_.Key) -ParameterString "-Port $($_.Value)" -Password $Password } exit } if ( $Unregister.IsPresent) { diff --git a/examples/HelloWorld/servicesettings.json b/examples/HelloWorld/servicesettings.json index 6d5e57be6..881b69b12 100644 --- a/examples/HelloWorld/servicesettings.json +++ b/examples/HelloWorld/servicesettings.json @@ -1,9 +1,9 @@ { - "PodePwshWorker ": { + "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\\PodePwshMonitorService.Prod.log", + "LogFilePath": "C:\\Users\\m_dan\\Documents\\GitHub\\Pode\\examples\\HelloWorld\\logs\\PodeMonitorService.Prod.log", "Quiet": true, "DisableTermination": true, "ShutdownWaitTimeMs": 30000 diff --git a/pode.build.ps1 b/pode.build.ps1 index f3152bfce..bcd80504e 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -452,7 +452,7 @@ Task Build BuildDeps, { } try { - Push-Location ./src/PodePwshMonitor + Push-Location ./src/PodeMonitor Invoke-PodeBuildDotnetMonitorSrvBuild } finally { diff --git a/src/PodePwshMonitor/Service/IPausableHostedService.cs b/src/PodeMonitor/IPausableHostedService.cs similarity index 96% rename from src/PodePwshMonitor/Service/IPausableHostedService.cs rename to src/PodeMonitor/IPausableHostedService.cs index 2a27855a3..5c526f4db 100644 --- a/src/PodePwshMonitor/Service/IPausableHostedService.cs +++ b/src/PodeMonitor/IPausableHostedService.cs @@ -1,4 +1,4 @@ -namespace Pode.Service +namespace PodeMonitor { /// /// Defines a contract for a hosted service that supports pausing and resuming. diff --git a/src/PodePwshMonitor/Service/PodePwshMonitor.cs b/src/PodeMonitor/PodeMonitor.cs similarity index 79% rename from src/PodePwshMonitor/Service/PodePwshMonitor.cs rename to src/PodeMonitor/PodeMonitor.cs index c6cbb594c..0bd2ea230 100644 --- a/src/PodePwshMonitor/Service/PodePwshMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -4,13 +4,13 @@ using System.IO.Pipes; using System.Threading; -namespace Pode.Service +namespace PodeMonitor { /// - /// The PodePwshMonitor class monitors and controls the execution of a Pode PowerShell process. + /// The PodeMonitor class monitors and controls the execution of a Pode PowerShell process. /// It communicates with the Pode process using named pipes. /// - public class PodePwshMonitor + public class PodeMonitor { private readonly object _syncLock = new(); // Synchronization lock for thread safety private Process _powerShellProcess; // PowerShell process instance @@ -31,10 +31,10 @@ public class PodePwshMonitor public int StartRetryDelayMs { get; } // Delay between retries in milliseconds /// - /// Initializes a new instance of the PodePwshMonitor class. + /// Initializes a new instance of the PodeMonitor class. /// - /// The configuration options for the PodePwshWorker. - public PodePwshMonitor(PodePwshWorkerOptions options) + /// The configuration options for the PodeMonitorWorker. + public PodeMonitor(PodeMonitorWorkerOptions options) { // Initialize configuration properties from options _scriptPath = options.ScriptPath; @@ -48,7 +48,7 @@ public PodePwshMonitor(PodePwshWorkerOptions options) // Generate a unique pipe name for communication _pipeName = $"PodePipe_{Guid.NewGuid()}"; - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, $"Initialized PodePwshMonitor with pipe name: {_pipeName}"); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName}"); } /// @@ -65,7 +65,7 @@ public void StartPowerShellProcess() // Log if the process is alive and log threshold is met if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process is Alive."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is Alive."); _lastLogTime = DateTime.Now; } return; @@ -88,8 +88,8 @@ public void StartPowerShellProcess() }; // Subscribe to output and error streams - _powerShellProcess.OutputDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); - _powerShellProcess.ErrorDataReceived += (sender, args) => PodePwshLogger.Log(LogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); + _powerShellProcess.OutputDataReceived += (sender, args) => PodeMonitorLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); + _powerShellProcess.ErrorDataReceived += (sender, args) => PodeMonitorLogger.Log(LogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); // Start the process _powerShellProcess.Start(); @@ -98,13 +98,13 @@ public void StartPowerShellProcess() // Log the process start time _lastLogTime = DateTime.Now; - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process started successfully."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process started successfully."); } catch (Exception ex) { // Log any errors during process start - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Failed to start Pode process: {ex.Message}"); - PodePwshLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to start Pode process: {ex.Message}"); + PodeMonitorLogger.Log(LogLevel.DEBUG, ex); } } } @@ -119,7 +119,7 @@ public void StopPowerShellProcess() { if (_powerShellProcess == null || _powerShellProcess.HasExited) { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process is not running."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is not running."); return; } @@ -130,24 +130,24 @@ public void StopPowerShellProcess() // Send shutdown message and wait for process exit SendPipeMessage("shutdown"); - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); // If process does not exit gracefully, forcefully terminate if (!_powerShellProcess.HasExited) { - PodePwshLogger.Log(LogLevel.WARN, "Server", Environment.ProcessId, "Pode process did not terminate gracefully, killing process."); + PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully, killing process."); _powerShellProcess.Kill(); } - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pode process stopped successfully."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process stopped successfully."); } } catch (Exception ex) { // Log errors during stop process - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Error stopping Pode process: {ex.Message}"); - PodePwshLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error stopping Pode process: {ex.Message}"); + PodeMonitorLogger.Log(LogLevel.DEBUG, ex); } finally { @@ -194,14 +194,14 @@ private void ExecutePipeCommand(string command) if (InitializePipeClient()) // Ensure pipe client is initialized { SendPipeMessage(command); - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, $"{command.ToUpper()} command sent to Pode process."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"{command.ToUpper()} command sent to Pode process."); } } catch (Exception ex) { // Log errors during command execution - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Error executing {command} command: {ex.Message}"); - PodePwshLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error executing {command} command: {ex.Message}"); + PodeMonitorLogger.Log(LogLevel.DEBUG, ex); } finally { @@ -234,7 +234,7 @@ private bool InitializePipeClient() if (!_pipeClient.IsConnected) { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Connecting to pipe server..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Connecting to pipe server..."); _pipeClient.Connect(10000); // Connect with a timeout of 10 seconds } @@ -254,8 +254,8 @@ private void SendPipeMessage(string message) } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, $"Error sending message to pipe: {ex.Message}"); - PodePwshLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error sending message to pipe: {ex.Message}"); + PodeMonitorLogger.Log(LogLevel.DEBUG, ex); } } diff --git a/src/PodePwshMonitor/PodeMonitor.csproj b/src/PodeMonitor/PodeMonitor.csproj similarity index 100% rename from src/PodePwshMonitor/PodeMonitor.csproj rename to src/PodeMonitor/PodeMonitor.csproj diff --git a/src/PodePwshMonitor/Service/PodePwshLogger.cs b/src/PodeMonitor/PodeMonitorLogger.cs similarity index 94% rename from src/PodePwshMonitor/Service/PodePwshLogger.cs rename to src/PodeMonitor/PodeMonitorLogger.cs index 7f92167e9..2a4e388b2 100644 --- a/src/PodePwshMonitor/Service/PodePwshLogger.cs +++ b/src/PodeMonitor/PodeMonitorLogger.cs @@ -2,7 +2,7 @@ using System; using System.IO; -namespace Pode.Service +namespace PodeMonitor { using System; using System.IO; @@ -16,7 +16,7 @@ public enum LogLevel CRITICAL // Critical errors indicating severe failures } - public static class PodePwshLogger + public static class PodeMonitorLogger { private static readonly object _logLock = new(); private static string logFilePath = "PodeService.log"; // Default log file path @@ -44,7 +44,7 @@ public static void Initialize(string filePath, LogLevel level) using (File.Create(logFilePath)) { } } - Log(LogLevel.INFO, "Server", Environment.ProcessId, "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", logFilePath, minLogLevel); + Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", logFilePath, minLogLevel); } catch (Exception ex) { @@ -52,7 +52,7 @@ public static void Initialize(string filePath, LogLevel level) } } - public static void Log(LogLevel level, string context ,int pid, string message = "", params object[] args) + public static void Log(LogLevel level, string context, int pid, string message = "", params object[] args) { if (level < minLogLevel || string.IsNullOrEmpty(message)) { @@ -131,7 +131,7 @@ public static void Log(LogLevel level, Exception exception, string message = nul int pid = Environment.ProcessId; // Build the log entry - string logEntry = $"{timestamp} [PID:{pid}] [{level}] [Server] {logMessage}"; + string logEntry = $"{timestamp} [PID:{pid}] [{level}] [PodeMonitor] {logMessage}"; // Thread-safe write to log file lock (_logLock) diff --git a/src/PodePwshMonitor/Service/PodePwshMain.cs b/src/PodeMonitor/PodeMonitorMain.cs similarity index 75% rename from src/PodePwshMonitor/Service/PodePwshMain.cs rename to src/PodeMonitor/PodeMonitorMain.cs index 91aa396b1..f1b5c5cc3 100644 --- a/src/PodePwshMonitor/Service/PodePwshMain.cs +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -11,14 +11,14 @@ using System.Runtime.Versioning; using Microsoft.Extensions.Logging; -namespace Pode.Service +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) + // Platform-dependent signal registration (for Linux/macOS) [LibraryImport("libc", EntryPoint = "signal")] private static partial int Signal(int signum, Action handler); @@ -26,7 +26,7 @@ public static partial class Program private const int SIGSTOP = 19; // Signal for pause private const int SIGCONT = 18; // Signal for continue private const int SIGHUP = 1; // Signal for restart - private static PodePwshWorker _workerInstance; // Global instance for managing worker operations + private static PodeMonitorWorker _workerInstance; // Global instance for managing worker operations /// /// Entry point for the Pode service. @@ -42,11 +42,11 @@ public static void Main(string[] args) .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) .Build(); - serviceName = config.GetSection("PodePwshWorker:Name").Value ?? serviceName; - string logFilePath = config.GetSection("PodePwshWorker:logFilePath").Value ?? "PodePwshMonitorService.log"; + serviceName = config.GetSection("PodeMonitorWorker:Name").Value ?? serviceName; + string logFilePath = config.GetSection("PodeMonitorWorker:logFilePath").Value ?? "PodeMonitorService.log"; // Initialize logger - PodePwshLogger.Initialize(logFilePath, LogLevel.INFO); + PodeMonitorLogger.Initialize(logFilePath, LogLevel.INFO); // Configure host builder var builder = CreateHostBuilder(args, customConfigFile); @@ -66,7 +66,7 @@ public static void Main(string[] args) } else { - PodePwshLogger.Log(LogLevel.WARN, "Server", Environment.ProcessId, "Unsupported platform. Exiting."); + PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Unsupported platform. Exiting."); } } @@ -85,31 +85,31 @@ private static IHostBuilder CreateHostBuilder(string[] args, string customConfig }) .ConfigureServices((context, services) => { - services.Configure(context.Configuration.GetSection("PodePwshWorker")); + services.Configure(context.Configuration.GetSection("PodeMonitorWorker")); - // Register PodePwshMonitor - services.AddSingleton(serviceProvider => + // Register PodeMonitor + services.AddSingleton(serviceProvider => { - var options = serviceProvider.GetRequiredService>().Value; - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Initializing PodePwshMonitor with options: {0}", JsonSerializer.Serialize(options)); - return new PodePwshMonitor(options); + var options = serviceProvider.GetRequiredService>().Value; + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initializing PodeMonitor with options: {0}", JsonSerializer.Serialize(options)); + return new PodeMonitor(options); }); - // Register PodePwshWorker and track the instance + // Register PodeMonitorWorker and track the instance services.AddSingleton(provider => { - var logger = provider.GetRequiredService>(); - var monitor = provider.GetRequiredService(); - var worker = new PodePwshWorker(logger, monitor); + var logger = provider.GetRequiredService>(); + var monitor = provider.GetRequiredService(); + var worker = new PodeMonitorWorker(logger, monitor); _workerInstance = worker; // Track the instance globally return worker; }); - // Add PodePwshWorker as a hosted service - services.AddHostedService(provider => provider.GetRequiredService()); + // Add PodeMonitorWorker as a hosted service + services.AddHostedService(provider => provider.GetRequiredService()); // Register IPausableHostedService - services.AddSingleton(provider => provider.GetRequiredService()); + services.AddSingleton(provider => provider.GetRequiredService()); }); } @@ -154,7 +154,7 @@ private static void ConfigureMacOS(IHostBuilder builder) private static void ConfigureWindows(IHostBuilder builder, string serviceName) { using var host = builder.Build(); - var service = new PodeWindowsService(host, serviceName); + var service = new PodeMonitorWindowsService(host, serviceName); ServiceBase.Run(service); } @@ -166,10 +166,10 @@ private static void HandlePause() { if (_workerInstance == null) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Pause requested, but _workerInstance is null."); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Pause requested, but _workerInstance is null."); return; } - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pausing service..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pausing service..."); _workerInstance?.OnPause(); } @@ -180,10 +180,10 @@ private static void HandleContinue() { if (_workerInstance == null) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Continue requested, but _workerInstance is null."); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Continue requested, but _workerInstance is null."); return; } - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Resuming service..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resuming service..."); _workerInstance?.OnContinue(); } @@ -194,10 +194,10 @@ private static void HandleRestart() { if (_workerInstance == null) { - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Restart requested, but _workerInstance is null."); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Restart requested, but _workerInstance is null."); return; } - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Restarting service..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restarting service..."); _workerInstance?.Restart(); } @@ -206,7 +206,7 @@ private static void HandleRestart() /// private static void Cleanup() { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Performing cleanup..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Performing cleanup..."); // Cleanup logic } #else diff --git a/src/PodePwshMonitor/Service/PodeWindowsService.cs b/src/PodeMonitor/PodeMonitorWindowsService.cs similarity index 70% rename from src/PodePwshMonitor/Service/PodeWindowsService.cs rename to src/PodeMonitor/PodeMonitorWindowsService.cs index 320a3939f..ef52094be 100644 --- a/src/PodePwshMonitor/Service/PodeWindowsService.cs +++ b/src/PodeMonitor/PodeMonitorWindowsService.cs @@ -6,23 +6,23 @@ using System.Runtime.Versioning; using System.Diagnostics; -namespace Pode.Service +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 PodeWindowsService : ServiceBase + 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 PodeWindowsService class. + /// Initializes a new instance of the PodeMonitorWindowsService class. /// /// The host instance managing the Pode application. /// The name of the Windows service. - public PodeWindowsService(IHost host, string serviceName) + 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 @@ -35,17 +35,17 @@ public PodeWindowsService(IHost host, string serviceName) /// Command-line arguments passed to the service. protected override void OnStart(string[] args) { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service starting..."); + PodeMonitorLogger.Log(LogLevel.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 - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service started successfully."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service started successfully."); } catch (Exception ex) { // Log the exception to the custom log - PodePwshLogger.Log(LogLevel.ERROR, ex, "Service startup failed."); + PodeMonitorLogger.Log(LogLevel.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}", @@ -61,10 +61,10 @@ protected override void OnStart(string[] args) /// protected override void OnStop() { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service stopping..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopping..."); base.OnStop(); // Call the base implementation _host.StopAsync().Wait(); // Stop the Pode host asynchronously and wait for it to complete - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service stopped successfully."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully."); } /// @@ -72,19 +72,19 @@ protected override void OnStop() /// protected override void OnPause() { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service pausing..."); + PodeMonitorLogger.Log(LogLevel.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) { - PodePwshLogger.Log(LogLevel.DEBUG, "Server", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodeMonitorLogger.Log(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnPause(); // Invoke the pause operation } else { - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); } } @@ -93,19 +93,19 @@ protected override void OnPause() /// protected override void OnContinue() { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service resuming..."); + PodeMonitorLogger.Log(LogLevel.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) { - PodePwshLogger.Log(LogLevel.DEBUG, "Server", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodeMonitorLogger.Log(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnContinue(); // Invoke the resume operation } else { - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); } } @@ -117,7 +117,7 @@ protected override void OnCustomCommand(int command) { if (command == CustomCommandRestart) { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Custom restart command received. Restarting service..."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Custom restart command received. Restarting service..."); var service = _host.Services.GetService(typeof(IPausableHostedService)); if (service != null) { @@ -125,7 +125,7 @@ protected override void OnCustomCommand(int command) } else { - PodePwshLogger.Log(LogLevel.ERROR, "Server", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService for restart."); + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService for restart."); } } else diff --git a/src/PodePwshMonitor/Service/PodePwshWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs similarity index 57% rename from src/PodePwshMonitor/Service/PodePwshWorker.cs rename to src/PodeMonitor/PodeMonitorWorker.cs index e88f14493..8609b0213 100644 --- a/src/PodePwshMonitor/Service/PodePwshWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -5,36 +5,33 @@ using System.Threading; using System.Threading.Tasks; -namespace Pode.Service +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 PodePwshWorker : BackgroundService, IPausableHostedService + public sealed class PodeMonitorWorker : BackgroundService, IPausableHostedService { // Logger instance for logging informational and error messages - private readonly ILogger _logger; + private readonly ILogger _logger; - // Instance of PodePwshMonitor to manage the Pode PowerShell process - private readonly PodePwshMonitor _pwshMonitor; - - // Tracks whether the worker is currently paused - private volatile bool _isPaused; + // 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; /// - /// Initializes a new instance of the PodePwshWorker class. + /// Initializes a new instance of the PodeMonitorWorker class. /// /// Logger instance for logging messages and errors. - /// Instance of PodePwshMonitor for managing the PowerShell process. - public PodePwshWorker(ILogger logger, PodePwshMonitor pwshMonitor) + /// Instance of PodeMonitor for managing the PowerShell process. + public PodeMonitorWorker(ILogger logger, PodeMonitor pwshMonitor) { _logger = logger; // Assign the logger - _pwshMonitor = pwshMonitor; // Assign the shared PodePwshMonitor instance - _logger.LogInformation("PodePwshWorker initialized with shared PodePwshMonitor."); + _pwshMonitor = pwshMonitor; // Assign the shared PodeMonitor instance + _logger.LogInformation("PodeMonitorWorker initialized with shared PodeMonitor."); } /// @@ -44,18 +41,11 @@ public PodePwshWorker(ILogger logger, PodePwshMonitor pwshMonito /// Cancellation token to signal when the worker should stop. protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "PodePwshWorker running at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.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) { - if (_isPaused) - { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Worker is paused. Waiting..."); - await Task.Delay(1000, stoppingToken); // Wait while paused - continue; - } - try { retryCount = 0; // Reset retry count on success @@ -66,12 +56,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) catch (Exception ex) { retryCount++; - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); + PodeMonitorLogger.Log(LogLevel.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) { - PodePwshLogger.Log(LogLevel.CRITICAL, "Server", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + PodeMonitorLogger.Log(LogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); break; } @@ -83,7 +73,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Task.Delay(10000, stoppingToken); } - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Monitoring loop has stopped."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Monitoring loop has stopped."); } /// @@ -92,7 +82,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) /// Cancellation token to signal when the stop should occur. public override async Task StopAsync(CancellationToken stoppingToken) { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); try { @@ -100,12 +90,12 @@ public override async Task StopAsync(CancellationToken stoppingToken) } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); } await base.StopAsync(stoppingToken); // Wait for the base StopAsync to complete - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); } /// @@ -113,16 +103,16 @@ public override async Task StopAsync(CancellationToken stoppingToken) /// public void Restart() { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); try { _pwshMonitor.RestartPowerShellProcess(); // Restart the process - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error during restart: {0}", ex.Message); + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during restart: {0}", ex.Message); } } @@ -131,20 +121,19 @@ public void Restart() /// public void OnPause() { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.SuspendPowerShellProcess(); // Send pause command to the process - _isPaused = true; // Update the paused state - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); AddOperationDelay("Pause"); // Delay to ensure stability } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); } } @@ -153,20 +142,19 @@ public void OnPause() /// public void OnContinue() { - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.ResumePowerShellProcess(); // Send resume command to the process - _isPaused = false; // Update the paused state - PodePwshLogger.Log(LogLevel.INFO, "Server", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); AddOperationDelay("Resume"); // Delay to ensure stability } catch (Exception ex) { - PodePwshLogger.Log(LogLevel.ERROR, ex, "Error during continue: {0}", ex.Message); + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during continue: {0}", ex.Message); } } @@ -176,7 +164,7 @@ public void OnContinue() /// The name of the operation (e.g., "Pause" or "Resume"). private void AddOperationDelay(string operation) { - PodePwshLogger.Log(LogLevel.DEBUG, "Server", Environment.ProcessId, "{0} operation completed. Adding delay of {1} ms.", operation, _delayMs); + PodeMonitorLogger.Log(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, "{0} operation completed. Adding delay of {1} ms.", operation, _delayMs); Thread.Sleep(_delayMs); // Introduce a delay } } diff --git a/src/PodePwshMonitor/Service/PodePwshWorkerOptions.cs b/src/PodeMonitor/PodeMonitorWorkerOptions.cs similarity index 95% rename from src/PodePwshMonitor/Service/PodePwshWorkerOptions.cs rename to src/PodeMonitor/PodeMonitorWorkerOptions.cs index a628388b1..7eb7e830c 100644 --- a/src/PodePwshMonitor/Service/PodePwshWorkerOptions.cs +++ b/src/PodeMonitor/PodeMonitorWorkerOptions.cs @@ -1,12 +1,12 @@ using System; -namespace Pode.Service +namespace PodeMonitor { /// - /// Configuration options for the PodePwshWorker service. + /// Configuration options for the PodeMonitorWorker service. /// These options determine how the worker operates, including paths, parameters, and retry policies. /// - public class PodePwshWorkerOptions + public class PodeMonitorWorkerOptions { /// diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 831ab119e..b6c863531 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3901,12 +3901,17 @@ function Invoke-PodeWinElevatedCommand { # Escape the arguments by replacing " with `" (escaping quotes) $escapedArguments = $Arguments -replace '"', '"""' + $psCredential = '' - # Combine command and arguments into a string for elevated execution - $escapedCommand = "`"$Command`" $escapedArguments" # 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}" ` diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 36a03eb11..79ce629bf 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -449,7 +449,7 @@ WantedBy=multi-user.target Registers a new Windows service to run a Pode-based PowerShell worker. .DESCRIPTION - The `Register-PodeWindowsService` function configures and registers a new Windows service to run a Pode-based PowerShell worker. + 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. @@ -484,7 +484,7 @@ WantedBy=multi-user.target Returns $true if successful. .EXAMPLE - Register-PodeWindowsService -Name "PodeExampleService" -DisplayName "Pode Example Service" ` + Register-PodeMonitorWindowsService -Name "PodeExampleService" -DisplayName "Pode Example Service" ` -BinPath "C:\Pode" -SettingsFile "C:\Pode\settings.json" ` -StartupType "Automatic" -Credential (Get-Credential) -Start -OsArchitecture "x64" @@ -492,7 +492,7 @@ WantedBy=multi-user.target generates the service, and starts it. .EXAMPLE - Register-PodeWindowsService -Name "PodeExampleService" -BinPath "C:\Pode" ` + Register-PodeMonitorWindowsService -Name "PodeExampleService" -BinPath "C:\Pode" ` -SettingsFile "C:\Pode\settings.json" -OsArchitecture "x64" Registers a new Windows service without credentials or immediate startup. @@ -503,7 +503,7 @@ WantedBy=multi-user.target - This is an internal function and may change in future releases of Pode. #> -function Register-PodeWindowsService { +function Register-PodeMonitorWindowsService { param( [string] $Name, @@ -893,7 +893,7 @@ function Test-PodeServiceIsActive { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is already running - return ($service.Status -ne 'Running') + return ($service.Status -ne 'Running') } return $false } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index c82917287..f8c53c3be 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -28,6 +28,12 @@ .PARAMETER ShutdownWaitTimeMs Maximum time in milliseconds to wait for the service to shut down gracefully before forcing termination. Defaults to 30,000 milliseconds. +.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 UserName Specifies the username under which the service will run by default is the current user. @@ -88,6 +94,12 @@ function Register-PodeService { [int] $ShutdownWaitTimeMs = 30000, + [int] + $StartMaxRetryCount = 3, + + [int] + $StartRetryDelayMs = 5000, + [string] $UserName, @@ -165,7 +177,7 @@ function Register-PodeService { # Generate the service settings JSON file $jsonContent = @{ - PodePwshWorker = @{ + PodeMonitorWorker = @{ ScriptPath = $ScriptPath PwshPath = $PwshPath ParameterString = $ParameterString @@ -174,6 +186,8 @@ function Register-PodeService { DisableTermination = $true ShutdownWaitTimeMs = $ShutdownWaitTimeMs Name = $Name + StartMaxRetryCount = $StartMaxRetryCount + StartRetryDelayMs = $StartRetryDelayMs } } @@ -198,7 +212,7 @@ function Register-PodeService { SecurityDescriptorSddl = $SecurityDescriptorSddl OsArchitecture = "win-$osArchitecture" } - $operation = Register-PodeWindowsService @param + $operation = Register-PodeMonitorWindowsService @param } elseif ($IsLinux) { $param = @{ From d959b34e8ac438f539d5debc90be5cf9064120c6 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 19 Nov 2024 10:16:03 -0800 Subject: [PATCH 38/93] fixes --- src/PodeMonitor/PipeNameGenerator.cs | 40 +++++++++ src/PodeMonitor/PodeMonitor.cs | 2 +- src/PodeMonitor/PodeMonitorMain.cs | 125 +++++++++------------------ src/PodeMonitor/PodeMonitorWorker.cs | 20 +++-- src/Private/Service.ps1 | 100 +++++++++++++++++++-- src/Public/Service.ps1 | 47 +++------- 6 files changed, 202 insertions(+), 132 deletions(-) create mode 100644 src/PodeMonitor/PipeNameGenerator.cs diff --git a/src/PodeMonitor/PipeNameGenerator.cs b/src/PodeMonitor/PipeNameGenerator.cs new file mode 100644 index 000000000..88fef97be --- /dev/null +++ b/src/PodeMonitor/PipeNameGenerator.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +namespace PodeMonitor +{ + public static class PipeNameGenerator + { + private const string WindowsPipePrefix = @"\\.\pipe\"; // Windows pipe namespace + 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 $"{WindowsPipePrefix}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}"); + + // 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/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 0bd2ea230..06d34511b 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -47,7 +47,7 @@ public PodeMonitor(PodeMonitorWorkerOptions options) StartRetryDelayMs = options.StartRetryDelayMs; // Generate a unique pipe name for communication - _pipeName = $"PodePipe_{Guid.NewGuid()}"; + _pipeName = PipeNameGenerator.GeneratePipeName(); PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName}"); } diff --git a/src/PodeMonitor/PodeMonitorMain.cs b/src/PodeMonitor/PodeMonitorMain.cs index f1b5c5cc3..3157423c8 100644 --- a/src/PodeMonitor/PodeMonitorMain.cs +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -18,14 +18,18 @@ namespace PodeMonitor /// public static partial class Program { - // Platform-dependent signal registration (for Linux/macOS) - [LibraryImport("libc", EntryPoint = "signal")] - private static partial int Signal(int signum, Action handler); + // 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 SIGSTOP = 19; // Signal for pause + 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 /// @@ -115,16 +119,17 @@ private static IHostBuilder CreateHostBuilder(string[] args, string customConfig #if ENABLE_LIFECYCLE_OPERATIONS /// - /// Configures the Pode service for Linux, including signal handling. + /// Configures the Pode service for linux, including signal handling. /// /// The host builder. - [SupportedOSPlatform("Linux")] + [SupportedOSPlatform("linux")] private static void ConfigureLinux(IHostBuilder builder) { - // Handle Linux signals for pause, resume, and restart - _ = Signal(SIGSTOP, _ => HandlePause()); - _ = Signal(SIGCONT, _ => HandleContinue()); - _ = Signal(SIGHUP, _ => HandleRestart()); + // Handle linux signals for pause, resume, and restart + Signal(SIGTSTP, HandleSignalStop); + Signal(SIGCONT, HandleSignalContinue); + Signal(SIGHUP, HandleSignalRestart); + builder.UseSystemd(); builder.Build().Run(); @@ -138,10 +143,10 @@ private static void ConfigureLinux(IHostBuilder builder) private static void ConfigureMacOS(IHostBuilder builder) { // Use launchd for macOS - _ = Signal(SIGSTOP, _ => HandlePause()); - _ = Signal(SIGCONT, _ => HandleContinue()); - _ = Signal(SIGHUP, _ => HandleRestart()); - + Signal(SIGTSTP, HandleSignalStop); + Signal(SIGCONT, HandleSignalContinue); + Signal(SIGHUP, HandleSignalRestart); + Signal(SIGTERM, HandleSignalTerminate); builder.Build().Run(); } @@ -156,92 +161,46 @@ private static void ConfigureWindows(IHostBuilder builder, string serviceName) using var host = builder.Build(); var service = new PodeMonitorWindowsService(host, serviceName); ServiceBase.Run(service); - } - /// - /// Handles the pause signal by pausing the Pode service. - /// - private static void HandlePause() + private static void HandleSignalStop(int signum) { - if (_workerInstance == null) - { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Pause requested, but _workerInstance is null."); - return; - } - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pausing service..."); - _workerInstance?.OnPause(); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received. Pausing service."); + HandlePause(); } - /// - /// Handles the continue signal by resuming the Pode service. - /// - private static void HandleContinue() + private static void HandleSignalTerminate(int signum) { - if (_workerInstance == null) - { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Continue requested, but _workerInstance is null."); - return; - } - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resuming service..."); - _workerInstance?.OnContinue(); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTERM received. Stopping service."); + HandleStop(); } - /// - /// Handles the restart signal by restarting the Pode service. - /// - private static void HandleRestart() + private static void HandleSignalContinue(int signum) { - if (_workerInstance == null) - { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Restart requested, but _workerInstance is null."); - return; - } - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restarting service..."); - _workerInstance?.Restart(); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGCONT received. Resuming service."); + HandleContinue(); } - /// - /// Performs cleanup operations before service termination. - /// - private static void Cleanup() + private static void HandleSignalRestart(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Performing cleanup..."); - // Cleanup logic + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGHUP received. Restarting service."); + 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 - /// - /// Configures the Pode service for Linux, including signal handling. - /// - /// The host builder. - [SupportedOSPlatform("Linux")] - private static void ConfigureLinux(IHostBuilder builder) - { - builder.UseSystemd(); - builder.Build().Run(); - } + [SupportedOSPlatform("linux")] + private static void ConfigureLinux(IHostBuilder builder) => builder.UseSystemd().Build().Run(); - /// - /// Configures the Pode service for macOS, including signal handling. - /// - /// The host builder. [SupportedOSPlatform("macOS")] - private static void ConfigureMacOS(IHostBuilder builder) - { - builder.Build().Run(); - } - - /// - /// Configures the Pode service for Windows, enabling pause and continue support. - /// - /// The host builder. - /// The name of the service. - private static void ConfigureWindows(IHostBuilder builder, string serviceName) - { - builder.UseWindowsService(); - builder.Build().Run(); - } + 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/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs index 8609b0213..13b49daf9 100644 --- a/src/PodeMonitor/PodeMonitorWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -81,21 +81,31 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) /// /// 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(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); + } + + + /// + /// Shutdown the Pode PowerShell process by sending a shutdown command. + /// + public void Shutdown() { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); try { - _pwshMonitor.StopPowerShellProcess(); // Stop the PowerShell process + _pwshMonitor.StopPowerShellProcess(); // Stop the process + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Stop message sent via pipe at: {0}", DateTimeOffset.Now); } catch (Exception ex) { PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); } - - await base.StopAsync(stoppingToken); // Wait for the base StopAsync to complete - - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); } /// diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 79ce629bf..e619e3b1a 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -260,9 +260,10 @@ function Register-PodeMacService { - + "@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -Encoding UTF8 @@ -647,6 +648,8 @@ function Confirm-PodeAdminPrivilege { #> function Test-PodeLinuxServiceIsRegistered { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $systemctlStatus = systemctl status $Name 2>&1 @@ -675,6 +678,8 @@ function Test-PodeLinuxServiceIsRegistered { #> function Test-PodeLinuxServiceIsActive { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $systemctlIsActive = systemctl is-active $Name 2>&1 @@ -703,6 +708,8 @@ function Test-PodeLinuxServiceIsActive { #> function Disable-PodeLinuxService { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $systemctlDisable = sudo systemctl disable $Name 2>&1 @@ -731,6 +738,8 @@ function Disable-PodeLinuxService { #> function Enable-PodeLinuxService { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $systemctlEnable = sudo systemctl enable $Name 2>&1 @@ -759,6 +768,8 @@ function Enable-PodeLinuxService { #> function Stop-PodeLinuxService { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $serviceStopInfo = sudo systemctl stop $Name 2>&1 @@ -787,6 +798,8 @@ function Stop-PodeLinuxService { #> function Start-PodeLinuxService { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $serviceStartInfo = sudo systemctl start $Name 2>&1 @@ -815,6 +828,8 @@ function Start-PodeLinuxService { #> function Test-PodeMacOsServiceIsRegistered { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $systemctlStatus = launchctl list $Name 2>&1 @@ -845,13 +860,15 @@ function Test-PodeMacOsServiceIsRegistered { #> function Test-PodeServiceIsRegistered { param( + [Parameter(Mandatory = $true)] + [string] $Name ) if ($IsLinux) { - return Test-PodeLinuxServiceIsRegistered + return Test-PodeLinuxServiceIsRegistered -Name $Name } if ($IsMacOS) { - return Test-PodeMacOsServiceIsRegistered + return Test-PodeMacOsServiceIsRegistered -Name $Name } if ($IsWindows) { $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" @@ -881,13 +898,15 @@ function Test-PodeServiceIsRegistered { #> function Test-PodeServiceIsActive { param( + [Parameter(Mandatory = $true)] + [string] $Name ) if ($IsLinux) { - return Test-PodeLinuxServiceIsActive + return Test-PodeLinuxServiceIsActive -Name $Name } if ($IsMacOS) { - return Test-PodeMacOsServiceIsActive + return Test-PodeMacOsServiceIsActive -Name $Name } if ($IsWindows) { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue @@ -920,6 +939,8 @@ function Test-PodeServiceIsActive { #> function Test-PodeMacOsServiceIsActive { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $serviceInfo = launchctl list $name @@ -948,6 +969,8 @@ PARAMETER Name #> function Get-PodeMacOsServicePid { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $serviceInfo = launchctl list $name @@ -976,6 +999,8 @@ function Get-PodeMacOsServicePid { #> function Disable-PodeMacOsService { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $systemctlDisable = launchctl unload "$HOME/Library/LaunchAgents/$Name.plist" 2>&1 @@ -1004,12 +1029,16 @@ function Disable-PodeMacOsService { #> function Stop-PodeMacOsService { param( + [Parameter(Mandatory = $true)] + [string] $Name ) - $serviceStopInfo = launchctl stop $Name 2>&1 - $success = $LASTEXITCODE -eq 0 - Write-Verbose -Message ($serviceStopInfo -join "`n") - return $success + + return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) + # $serviceStopInfo = launchctl stop $Name 2>&1 + # $success = $LASTEXITCODE -eq 0 + # Write-Verbose -Message ($serviceStopInfo -join "`n") + # return $success } <# @@ -1032,10 +1061,63 @@ function Stop-PodeMacOsService { #> function Start-PodeMacOsService { param( + [Parameter(Mandatory = $true)] + [string] $Name ) $serviceStartInfo = launchctl start $Name 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($serviceStartInfo -join "`n") return $success +} + + +function Send-PodeServiceSignal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [ValidateSet('SIGTSTP', 'SIGCONT', 'SIGHUP', 'SIGTERM')] + [string] + $Signal + ) + + # Standardize service naming for Linux/macOS + $nameService = $(if ($IsMacOS) { "pode.$Name.service".Replace(' ', '_') }else { "$Name.service".Replace(' ', '_') }) + + $signalMap = @{ + 'SIGTSTP' = 20 + 'SIGCONT' = 18 + 'SIGHUP' = 1 + 'SIGTERM' = 15 + } + $level = $signalMap[$Signal] + # Check if the service is registered + if ((Test-PodeServiceIsRegistered -Name $nameService)) { + if ((Test-PodeServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' is active. Sending $Signal signal." + $svc = Get-PodeService -Name $Name + if ($svc.Sudo) { + sudo /bin/kill -$($level) $svc.Pid + } + else { + /bin/kill -$($level) $svc.Pid + } + $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 { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + } \ No newline at end of file diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index f8c53c3be..9ae421e7b 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -480,7 +480,7 @@ function Stop-PodeService { if ((Test-PodeMacOsServiceIsRegistered -Name $nameService)) { # Check if the service is active if ((Test-PodeMacOsServiceIsActive $nameService)) { - if ((Stop-PodeMacOsService $nameService)) { + if ((Stop-PodeMacOsService $Name)) { if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$Name' stopped successfully." return $true @@ -565,9 +565,8 @@ function Suspend-PodeService { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } } - else { - # Feature not supported on Linux or macOS - throw ($PodeLocale.featureNotSupportedException -f 'Suspend Service') + elseif ($IsLinux -or $IsMacOS) { + Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP' } } catch { @@ -635,10 +634,8 @@ function Resume-PodeService { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } } - - else { - # Feature not supported on Linux or macOS - throw ($PodeLocale.featureNotSupportedException -f 'Resume Service') + elseif ($IsLinux -or $IsMacOS) { + Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' } } catch { @@ -826,7 +823,7 @@ function Unregister-PodeService { if ((Test-PodeMacOsServiceIsActive -Name $nameService)) { if ($Force.IsPresent) { #Stop the service - if (( Stop-PodeMacOsService -Name $nameService)) { + if (( Stop-PodeMacOsService -Name $Name)) { # Check if the service is active if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$Name' stopped successfully." @@ -956,6 +953,7 @@ function Get-PodeService { Name = $Name Status = $status Pid = $service.ProcessId + Sudo = $true } } else { @@ -1006,13 +1004,12 @@ function Get-PodeService { Name = $Name Status = $status Pid = $servicePid + Sudo = $true } } else { Write-Verbose -Message "Service '$nameService' not found." } - - } catch { $_ | Write-PodeErrorLog @@ -1027,12 +1024,15 @@ function Get-PodeService { # Check if the service exists on macOS (launchctl) if ((Test-PodeMacOsServiceIsRegistered $nameService )) { $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match + + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) # Check if the service has a PID entry if ($servicePid -ne 0) { return @{ Name = $Name Status = 'Running' Pid = $servicePid + Sudo = $sudo } } else { @@ -1040,6 +1040,7 @@ function Get-PodeService { Name = $Name Status = 'Stopped' Pid = 0 + Sudo = $sudo } } } @@ -1122,29 +1123,7 @@ function Restart-PodeService { } } elseif ($IsLinux -or $IsMacOS) { - # Standardize service naming for Linux/macOS - $nameService = $(if ($IsMacOS) { "pode.$Name.service".Replace(' ', '_') }else { "$Name.service".Replace(' ', '_') }) - # Check if the service is registered - if ((Test-PodeServiceIsRegistered -Name $nameService)) { - if ((Test-PodeServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' is active. Sending SIGHUP signal." - $svc = Get-PodeService -Name $nameService - sudo /bin/kill -SIGHUP $svc.ProcessId - Write-Verbose -Message "SIGHUP signal sent to service '$Name'." - } - else { - Write-Verbose -Message "Service '$Name' is not running." - } - } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) - } - } - else { - # Unsupported platform - Write-Error -Message "Unsupported platform. Unable to restart service '$Name'." - return $false + Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP' } } catch { From 9bb9747f30d750e8c237d2c59c563cdc4ce7ab14 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 19 Nov 2024 12:34:34 -0800 Subject: [PATCH 39/93] minor fixes --- examples/HelloService/HelloService.ps1 | 34 ++++++++++---------- src/PodeMonitor/PipeNameGenerator.cs | 5 ++- src/PodeMonitor/PodeMonitorMain.cs | 12 +++---- src/PodeMonitor/PodeMonitorWindowsService.cs | 2 -- src/Public/Service.ps1 | 1 + 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 3f7212b51..a948dd86b 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -3,7 +3,7 @@ PowerShell script to register, start, stop, query, and unregister a Pode service, with a basic server setup. .DESCRIPTION - This script manages a Pode service named 'Hello Service3' with commands to register, start, stop, query, + This script manages a Pode service named 'Hello Service' with commands to register, start, stop, query, and unregister the service. Additionally, it sets up a Pode server that listens on port 8080 and includes a simple GET route that responds with 'Hello, Service!'. @@ -14,34 +14,34 @@ # Response: 'Hello, Service!' .PARAMETER Register - Registers the 'Hello Service3' with Pode. + Registers the 'Hello Service' with Pode. .PARAMETER Password A secure password for the service account (Windows only). If omitted, the service account will be 'NT AUTHORITY\SYSTEM'. .PARAMETER Unregister - Unregisters the 'Hello Service3' from Pode. Use with the -Force switch to forcefully unregister the service. + Unregisters the 'Hello Service' 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 'Hello Service3'. + Starts the 'Hello Service'. .PARAMETER Stop - Stops the 'Hello Service3'. + Stops the 'Hello Service'. .PARAMETER Query - Queries the status of the 'Hello Service3'. + Queries the status of the 'Hello Service'. .PARAMETER Suspend - Suspend the 'Hello Service3'. + Suspend the 'Hello Service'. .PARAMETER Resume - Resume the 'Hello Service3'. + Resume the 'Hello Service'. .PARAMETER Restart - Restart the 'Hello Service3'. + Restart the 'Hello Service'. .EXAMPLE Register the service: @@ -141,40 +141,40 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service3' -ParameterString "-Port $Port" -Password $Password + Register-PodeService -Name 'Hello Service' -ParameterString "-Port $Port" -Password $Password exit } if ( $Unregister.IsPresent) { - Unregister-PodeService -Name 'Hello Service3' -Force:$Force + Unregister-PodeService -Name 'Hello Service' -Force:$Force exit } if ($Start.IsPresent) { - Start-PodeService -Name 'Hello Service3' + Start-PodeService -Name 'Hello Service' exit } if ($Stop.IsPresent) { - Stop-PodeService -Name 'Hello Service3' + Stop-PodeService -Name 'Hello Service' exit } if ($Suspend.IsPresent) { - Suspend-PodeService -Name 'Hello Service3' + Suspend-PodeService -Name 'Hello Service' exit } if ($Resume.IsPresent) { - Resume-PodeService -Name 'Hello Service3' + Resume-PodeService -Name 'Hello Service' exit } if ($Query.IsPresent) { - Get-PodeService -Name 'Hello Service3' + Get-PodeService -Name 'Hello Service' exit } if ($Restart.IsPresent) { - Restart-PodeService -Name 'Hello Service3' + Restart-PodeService -Name 'Hello Service' exit } diff --git a/src/PodeMonitor/PipeNameGenerator.cs b/src/PodeMonitor/PipeNameGenerator.cs index 88fef97be..e178b2387 100644 --- a/src/PodeMonitor/PipeNameGenerator.cs +++ b/src/PodeMonitor/PipeNameGenerator.cs @@ -3,8 +3,7 @@ namespace PodeMonitor { public static class PipeNameGenerator - { - private const string WindowsPipePrefix = @"\\.\pipe\"; // Windows pipe namespace + { private const int MaxUnixPathLength = 104; // Max length for Unix domain sockets on macOS private const string UnixTempDir = "/tmp"; // Short temporary directory for Unix systems @@ -16,7 +15,7 @@ public static string GeneratePipeName() if (OperatingSystem.IsWindows()) { // Use Windows named pipe format - return $"{WindowsPipePrefix}PodePipe_{uniqueId}"; + return $"PodePipe_{uniqueId}"; } else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { diff --git a/src/PodeMonitor/PodeMonitorMain.cs b/src/PodeMonitor/PodeMonitorMain.cs index 3157423c8..65c6db5c3 100644 --- a/src/PodeMonitor/PodeMonitorMain.cs +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -31,7 +31,7 @@ public static partial class Program private const int SIGTERM = 15; // Signal for gracefully terminate a process. private static PodeMonitorWorker _workerInstance; // Global instance for managing worker operations - + private static bool _terminating = false; /// /// Entry point for the Pode service. /// @@ -40,7 +40,6 @@ public static void Main(string[] args) { string customConfigFile = args.Length > 0 ? args[0] : "srvsettings.json"; // Default config file string serviceName = "PodeService"; - // Load configuration IConfigurationRoot config = new ConfigurationBuilder() .AddJsonFile(customConfigFile, optional: false, reloadOnChange: true) @@ -129,8 +128,6 @@ private static void ConfigureLinux(IHostBuilder builder) Signal(SIGTSTP, HandleSignalStop); Signal(SIGCONT, HandleSignalContinue); Signal(SIGHUP, HandleSignalRestart); - - builder.UseSystemd(); builder.Build().Run(); } @@ -165,13 +162,16 @@ private static void ConfigureWindows(IHostBuilder builder, string serviceName) private static void HandleSignalStop(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received. Pausing service."); - HandlePause(); + if(!_terminating){ + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received. Pausing service."); + HandlePause(); + } } private static void HandleSignalTerminate(int signum) { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTERM received. Stopping service."); + _terminating=true; HandleStop(); } diff --git a/src/PodeMonitor/PodeMonitorWindowsService.cs b/src/PodeMonitor/PodeMonitorWindowsService.cs index ef52094be..20b0424a0 100644 --- a/src/PodeMonitor/PodeMonitorWindowsService.cs +++ b/src/PodeMonitor/PodeMonitorWindowsService.cs @@ -61,10 +61,8 @@ protected override void OnStart(string[] args) /// protected override void OnStop() { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopping..."); base.OnStop(); // Call the base implementation _host.StopAsync().Wait(); // Stop the Pode host asynchronously and wait for it to complete - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully."); } /// diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 9ae421e7b..0498f9c40 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -1115,6 +1115,7 @@ function Restart-PodeService { } else { Write-Verbose -Message "Service '$Name' is not running." + return $false } } else { From ac2fd828e00ff624cf79792bf838e140cb646512 Mon Sep 17 00:00:00 2001 From: Max Daneri Date: Tue, 19 Nov 2024 17:19:58 -0500 Subject: [PATCH 40/93] Linux fixes --- src/PodeMonitor/PipeNameGenerator.cs | 5 ++- src/PodeMonitor/PodeMonitorMain.cs | 15 +++---- src/PodeMonitor/PodeMonitorWorker.cs | 60 ++++++++++++++++++---------- src/Private/Service.ps1 | 24 +++++++---- src/Public/Service.ps1 | 9 +++-- 5 files changed, 71 insertions(+), 42 deletions(-) diff --git a/src/PodeMonitor/PipeNameGenerator.cs b/src/PodeMonitor/PipeNameGenerator.cs index e178b2387..26b104dec 100644 --- a/src/PodeMonitor/PipeNameGenerator.cs +++ b/src/PodeMonitor/PipeNameGenerator.cs @@ -3,7 +3,7 @@ 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 @@ -20,7 +20,8 @@ public static string GeneratePipeName() 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 = 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) diff --git a/src/PodeMonitor/PodeMonitorMain.cs b/src/PodeMonitor/PodeMonitorMain.cs index 65c6db5c3..dfe3a7516 100644 --- a/src/PodeMonitor/PodeMonitorMain.cs +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -31,7 +31,7 @@ public static partial class Program private const int SIGTERM = 15; // Signal for gracefully terminate a process. private static PodeMonitorWorker _workerInstance; // Global instance for managing worker operations - private static bool _terminating = false; + /// /// Entry point for the Pode service. /// @@ -162,28 +162,25 @@ private static void ConfigureWindows(IHostBuilder builder, string serviceName) private static void HandleSignalStop(int signum) { - if(!_terminating){ - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received. Pausing service."); - HandlePause(); - } + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received."); + HandlePause(); } private static void HandleSignalTerminate(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTERM received. Stopping service."); - _terminating=true; + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTERM received."); HandleStop(); } private static void HandleSignalContinue(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGCONT received. Resuming service."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGCONT received."); HandleContinue(); } private static void HandleSignalRestart(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGHUP received. Restarting service."); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGHUP received."); HandleRestart(); } diff --git a/src/PodeMonitor/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs index 13b49daf9..36d3bccbf 100644 --- a/src/PodeMonitor/PodeMonitorWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -22,6 +22,11 @@ public sealed class PodeMonitorWorker : BackgroundService, IPausableHostedServic // Delay in milliseconds to prevent rapid consecutive operations private readonly int _delayMs = 5000; + private bool _terminating=false; + + private bool _suspended=false; + + private bool _running=false; /// /// Initializes a new instance of the PodeMonitorWorker class. /// @@ -52,6 +57,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Start the Pode PowerShell process _pwshMonitor.StartPowerShellProcess(); + _running=true; } catch (Exception ex) { @@ -95,17 +101,21 @@ public override async Task StopAsync(CancellationToken stoppingToken) /// public void Shutdown() { + if((! _terminating )&& _running){ + + _terminating=true; PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); try { _pwshMonitor.StopPowerShellProcess(); // Stop the process + _running=false; PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Stop message sent via pipe at: {0}", DateTimeOffset.Now); } catch (Exception ex) { PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); - } + }} } /// @@ -113,16 +123,19 @@ public void Shutdown() /// public void Restart() { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); + if((! _terminating )&& _running){ - try - { - _pwshMonitor.RestartPowerShellProcess(); // Restart the process - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); - } - catch (Exception ex) - { - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during restart: {0}", ex.Message); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); + + try + { + _pwshMonitor.RestartPowerShellProcess(); // Restart the process + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + } + catch (Exception ex) + { + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during restart: {0}", ex.Message); + } } } @@ -131,12 +144,13 @@ public void Restart() /// public void OnPause() { + if((! _terminating )&& _running){ PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.SuspendPowerShellProcess(); // Send pause command to the process - +_suspended=true; PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); AddOperationDelay("Pause"); // Delay to ensure stability @@ -145,6 +159,7 @@ public void OnPause() { PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); } + } } /// @@ -152,19 +167,22 @@ public void OnPause() /// public void OnContinue() { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); + if((! _terminating )&& _suspended){ + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); - try - { - _pwshMonitor.ResumePowerShellProcess(); // Send resume command to the process + try + { + _pwshMonitor.ResumePowerShellProcess(); // Send resume command to the process - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); +_suspended=false; + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); - AddOperationDelay("Resume"); // Delay to ensure stability - } - catch (Exception ex) - { - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during continue: {0}", ex.Message); + AddOperationDelay("Resume"); // Delay to ensure stability + } + catch (Exception ex) + { + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during continue: {0}", ex.Message); + } } } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index e619e3b1a..079557c8b 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -60,7 +60,7 @@ function Start-PodeServiceHearthbeat { $scriptBlock = { while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { - Write-PodeHost -Message "Start client receiver for pipe $($PodeContext.Server.Service.PipeName)" -Force + Write-PodeHost -Message "Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force Write-PodeHost -Message "Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force Write-PodeHost -Message "Total Number of Restart: $(Get-PodeServerRestartCount)" -Force @@ -96,6 +96,8 @@ function Start-PodeServiceHearthbeat { # Process 'shutdown' message Write-PodeHost -Message 'Server requested shutdown. Closing Pode ...' -Force Close-PodeServer # Gracefully stop Pode server + Start-Sleep 1 + Write-PodeHost -Message "Closing Service Monitoring Heartbeat" -Force return # Exit the loop } @@ -103,7 +105,10 @@ function Start-PodeServiceHearthbeat { # Process 'restart' message Write-PodeHost -Message 'Server requested restart. Restarting Pode ...' -Force Restart-PodeServer # Restart Pode server - return # Exit the loop + Start-Sleep 1 + Write-PodeHost -Message "Closing Service Monitoring Heartbeat" -Force + return + # Exit the loop } 'suspend' { @@ -124,6 +129,7 @@ function Start-PodeServiceHearthbeat { } } + break } } catch { @@ -133,9 +139,11 @@ function Start-PodeServiceHearthbeat { finally { $reader.Dispose() $pipeStream.Dispose() # Always dispose of the pipe stream when done + Write-PodeHost -Message "Disposing Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force } } + Write-PodeHost -Message "Closing Service Monitoring Heartbeat" -Force } # Assign a name to the Pode service @@ -772,10 +780,12 @@ function Stop-PodeLinuxService { [string] $Name ) - $serviceStopInfo = sudo systemctl stop $Name 2>&1 - $success = $LASTEXITCODE -eq 0 - Write-Verbose -Message ($serviceStopInfo -join "`n") - return $success + + #return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) + $serviceStopInfo = sudo systemctl stop $("$Name.service".Replace(' ', '_')) 2>&1 + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($serviceStopInfo -join "`n") + return $success } <# @@ -1119,5 +1129,5 @@ function Send-PodeServiceSignal { # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } - +return $false } \ No newline at end of file diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 0498f9c40..606589dde 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -453,7 +453,7 @@ function Stop-PodeService { # Check if the service is active if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { #Stop the service - if (( Stop-PodeLinuxService -Name $nameService)) { + if (( Stop-PodeLinuxService -Name $Name)) { # Check if the service is active if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$Name' stopped successfully." @@ -755,7 +755,7 @@ function Unregister-PodeService { if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { if ($Force.IsPresent) { #Stop the service - if (( Stop-PodeLinuxService -Name $nameService)) { + if (( Stop-PodeLinuxService -Name $Name)) { # Check if the service is active if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$Name' stopped successfully." @@ -1124,7 +1124,10 @@ function Restart-PodeService { } } elseif ($IsLinux -or $IsMacOS) { - Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP' + if( !(Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP')){ + Write-Verbose -Message "Service '$Name' is not running." + return $false + } } } catch { From 0978c68160e83b9aef1e1a14a4339cc89f3bd303 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 19 Nov 2024 14:23:43 -0800 Subject: [PATCH 41/93] fixes --- src/PodeMonitor/PipeNameGenerator.cs | 2 +- src/Private/Service.ps1 | 23 +++++++++++------------ src/Public/Service.ps1 | 8 ++++---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/PodeMonitor/PipeNameGenerator.cs b/src/PodeMonitor/PipeNameGenerator.cs index 26b104dec..f7d2c3fa7 100644 --- a/src/PodeMonitor/PipeNameGenerator.cs +++ b/src/PodeMonitor/PipeNameGenerator.cs @@ -21,7 +21,7 @@ public static string GeneratePipeName() { // Use Unix domain socket format with a shorter temp directory //string pipePath = Path.Combine(UnixTempDir, $"PodePipe_{uniqueId}"); - string pipePath = "PodePipe_{uniqueId}"; + string pipePath = "PodePipe_{uniqueId}"; // Ensure the path is within the allowed length for Unix domain sockets if (pipePath.Length > MaxUnixPathLength) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 079557c8b..ee1560d5c 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -22,7 +22,6 @@ function Test-PodeServiceEnabled { return $PodeContext.Server.ContainsKey('Service') } - <# .SYNOPSIS Starts the Pode Service Heartbeat using a named pipe for communication with a C# service. @@ -96,8 +95,8 @@ function Start-PodeServiceHearthbeat { # Process 'shutdown' message Write-PodeHost -Message 'Server requested shutdown. Closing Pode ...' -Force Close-PodeServer # Gracefully stop Pode server - Start-Sleep 1 - Write-PodeHost -Message "Closing Service Monitoring Heartbeat" -Force + Start-Sleep 1 + Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force return # Exit the loop } @@ -105,10 +104,10 @@ function Start-PodeServiceHearthbeat { # Process 'restart' message Write-PodeHost -Message 'Server requested restart. Restarting Pode ...' -Force Restart-PodeServer # Restart Pode server - Start-Sleep 1 - Write-PodeHost -Message "Closing Service Monitoring Heartbeat" -Force - return - # Exit the loop + Start-Sleep 1 + Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force + return + # Exit the loop } 'suspend' { @@ -143,7 +142,7 @@ function Start-PodeServiceHearthbeat { } } - Write-PodeHost -Message "Closing Service Monitoring Heartbeat" -Force + Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force } # Assign a name to the Pode service @@ -783,9 +782,9 @@ function Stop-PodeLinuxService { #return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) $serviceStopInfo = sudo systemctl stop $("$Name.service".Replace(' ', '_')) 2>&1 - $success = $LASTEXITCODE -eq 0 - Write-Verbose -Message ($serviceStopInfo -join "`n") - return $success + $success = $LASTEXITCODE -eq 0 + Write-Verbose -Message ($serviceStopInfo -join "`n") + return $success } <# @@ -1129,5 +1128,5 @@ function Send-PodeServiceSignal { # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } -return $false + return $false } \ No newline at end of file diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 606589dde..633270eae 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -1124,10 +1124,10 @@ function Restart-PodeService { } } elseif ($IsLinux -or $IsMacOS) { - if( !(Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP')){ - Write-Verbose -Message "Service '$Name' is not running." - return $false - } + if ( !(Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP')) { + Write-Verbose -Message "Service '$Name' is not running." + return $false + } } } catch { From cc5787e7974059a2c33182d41263f362df97245f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 19 Nov 2024 20:43:09 -0800 Subject: [PATCH 42/93] Mac fixes --- src/Public/Service.ps1 | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 633270eae..b44502c85 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -481,12 +481,14 @@ function Stop-PodeService { # Check if the service is active if ((Test-PodeMacOsServiceIsActive $nameService)) { if ((Stop-PodeMacOsService $Name)) { - if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - return $true + for($i=0;$i -lt 30; $i++){ + if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { + Write-Verbose -Message "Service '$Name' stopped successfully." + return $true + } + Start-Sleep 1 } } - # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) @@ -509,8 +511,6 @@ function Stop-PodeService { return $true } - - <# .SYNOPSIS Suspends a specified service on Windows systems. @@ -566,7 +566,7 @@ function Suspend-PodeService { } } elseif ($IsLinux -or $IsMacOS) { - Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP' + return Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP' } } catch { @@ -577,8 +577,6 @@ function Suspend-PodeService { return $true } - - <# .SYNOPSIS Resumes a specified service on Windows systems. @@ -607,7 +605,6 @@ function Resume-PodeService { # Ensure the script is running with the necessary administrative/root privileges. # Exits the script if the current user lacks the required privileges. Confirm-PodeAdminPrivilege - if ($IsWindows) { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue @@ -635,7 +632,7 @@ function Resume-PodeService { } } elseif ($IsLinux -or $IsMacOS) { - Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' + return Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' } } catch { From 6c2b883e1fbe084882df56a726fc3fa391fddb3c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 20 Nov 2024 11:25:50 -0800 Subject: [PATCH 43/93] add DisableLifecycleServiceOperations to build --- pode.build.ps1 | 7 +++++-- src/Public/Routes.ps1 | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index bcd80504e..af1d546c9 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -19,7 +19,10 @@ param( $ReleaseNoteVersion, [string] - $UICulture = 'en-US' + $UICulture = 'en-US', + + [switch] + $DisableLifecycleServiceOperations ) # Fix for PS7.5 Preview - https://github.com/PowerShell/PowerShell/issues/23868 @@ -169,7 +172,7 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { $DefineConstants = @() $ParamConstants = '' - if (!$DisableSuspendSupport) { + if (!$DisableLifecycleServiceOperations) { $DefineConstants += 'ENABLE_LIFECYCLE_OPERATIONS' } diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 9bc6db522..575fede7f 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -452,8 +452,8 @@ function Add-PodeRoute { }) - if ($PodeContext.Server.OpenAPI.Routes -notcontains $OpenApiPath ) { - $PodeContext.Server.OpenAPI.Routes += $OpenApiPath + if ($PodeContext.Server.OpenAPI.Routes -notcontains $Path ) { + $PodeContext.Server.OpenAPI.Routes += $Path } From 66e48179ea675ad6bc7c30f01180f69fc7562737 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 20 Nov 2024 12:10:13 -0800 Subject: [PATCH 44/93] fix Test-PodeAdminPrivilege --- src/Private/Helpers.ps1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index b6c863531..e4093f281 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3803,7 +3803,11 @@ function Test-PodeAdminPrivilege { try { # Check if the operating system is Windows if ($IsWindows) { - $principal = [Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()) + + # Retrieve the current Windows identity and token + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($identity) + if ($null -eq $principal) { return $false } @@ -3813,6 +3817,11 @@ function Test-PodeAdminPrivilege { return $true } + # Check if the token is elevated + if ($identity.IsSystem || $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 From 44f87686b88c07af01b88f35a05555b8bb7c157d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 21 Nov 2024 09:45:19 -0800 Subject: [PATCH 45/93] improve workflow --- docs/Hosting/RunAsService.md | 257 +++++++++++++++------------ src/PodeMonitor/PodeMonitor.cs | 73 +++++++- src/PodeMonitor/PodeMonitorLogger.cs | 100 ++++++----- src/PodeMonitor/PodeMonitorWorker.cs | 106 +++++++---- src/Private/Context.ps1 | 6 +- src/Private/Server.ps1 | 13 +- src/Private/Service.ps1 | 18 +- 7 files changed, 366 insertions(+), 207 deletions(-) diff --git a/docs/Hosting/RunAsService.md b/docs/Hosting/RunAsService.md index d06541826..e9525c611 100644 --- a/docs/Hosting/RunAsService.md +++ b/docs/Hosting/RunAsService.md @@ -1,34 +1,37 @@ # Using Pode as a Service -Pode now provides built-in functions to easily manage services across platforms (Windows, Linux, macOS). These functions allow you to register, start, stop, query, and unregister Pode services in a cross-platform way. +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. + +--- ## Registering a Service -You can register a Pode-based service using the `Register-PodeService` function, which will create the necessary service files and configurations for your system. +The `Register-PodeService` function creates the necessary service files and configurations for your system. #### Example: + ```powershell Register-PodeService -Name "HelloService" -Description "Example Pode Service" -ParameterString "-Verbose" -Start ``` -This command registers a service named "HelloService" and starts it immediately after registration. The service runs your Pode script with the specified parameters. +This registers a service named "HelloService" and starts it immediately after registration. The service runs your Pode script with the specified parameters. ### `Register-PodeService` Parameters -The `Register-PodeService` function provides several parameters to customize your service registration across Windows, Linux, and macOS: +The `Register-PodeService` function offers several parameters to customize your service registration: - **`-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." + 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)". + 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'. + 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. @@ -37,215 +40,233 @@ The `Register-PodeService` function provides several parameters to customize you 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 milliseconds. + 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`. - **`-UserName`** *(string)*: - Specifies the username under which the service will run by default is the current user. + Specifies the username under which the service will run. Defaults to the current user. + +- **`-CreateUser`** *(switch)*: + Creates the user if it does not exist (Linux only). - **`-Start`** *(switch)*: - A switch to start the service immediately after registration. + 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'. + 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). + A security descriptor in SDDL format specifying the permissions for the service (Windows only). - **`-SettingsPath`** *(string)*: - Specifies the directory to store the service configuration file (`_svcsettings.json`). If not provided, a default directory is used. + Directory to store the service configuration file (`_svcsettings.json`). Defaults to a directory under the script path. - **`-LogPath`** *(string)*: - Specifies the path for the service log files. If not provided, a default log directory is used. + Path for the service log files. Defaults to a directory under the script path. --- ## Starting a Service -Once a service is registered, you can start it using the `Start-PodeService` function. +You can start a registered service using the `Start-PodeService` function. #### Example: + ```powershell Start-PodeService -Name "HelloService" ``` -This returns $true if the service is started successfully, $false otherwise. +This returns `$true` if the service starts successfully, `$false` otherwise. + +--- ## Stopping a Service -To stop a running Pode service, you can use the `Stop-PodeService` function. +To stop a running service, use the `Stop-PodeService` function. #### Example: + ```powershell Stop-PodeService -Name "HelloService" ``` -This returns $true if it was stopped, $false otherwise. -## Querying a Service +This returns `$true` if the service stops successfully, `$false` otherwise. + +--- + +## Suspending a Service -To check the status of a service (whether it's running or stopped), use the `Get-PodeService` function. +Suspend a running service (Windows only) with the `Suspend-PodeService` function. #### Example: -```powershell -Get-PodeService -Name "HelloService" -``` -This returns a hashtable with the service name and status. ```powershell -Name Value ----- ----- -Status Running -Pid 17576 -Name HelloService +Suspend-PodeService -Name "HelloService" ``` -## Unregistering a Service +This pauses the service, returning `$true` if successful. -When you're done with a service, you can unregister it using the `Unregister-PodeService` function. You can also forcefully stop and remove a service using the `-Force` parameter. +--- + +## Resuming a Service + +Resume a suspended service (Windows only) using the `Resume-PodeService` function. #### Example: + ```powershell -Unregister-PodeService -Name "HelloService" -Force +Resume-PodeService -Name "HelloService" ``` -This returns $true if it was unregistered successfully, $false otherwise. +This resumes the service, returning `$true` if successful. +--- +## Querying a Service -# Alternative Methods for Windows and Linux - -If you prefer to manually manage Pode as a service or if you're working in an environment where the Pode functions are unavailable, you can still use the traditional methods for managing services on Windows and Linux. - -#### Windows (NSSM): +To check the status of a service, use the `Get-PodeService` function. -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: +#### Example: ```powershell -choco install nssm -y +Get-PodeService -Name "HelloService" ``` -Once installed, you'll need to set the location of the `pwsh` or `powershell` executables as a variable: +This returns a hashtable with the service details: ```powershell -$exe = (Get-Command pwsh.exe).Source - -# or - -$exe = (Get-Command powershell.exe).Source +Name Value +---- ----- +Status Running +Pid 17576 +Name HelloService +Sudo True ``` -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: +--- -```powershell -$name = 'Pode Web Server' -$file = 'C:\Pode\Server.ps1' -$arg = "-ExecutionPolicy Bypass -NoProfile -Command `"$($file)`"" -``` +## Restarting a Service + +Restart a running service using the `Restart-PodeService` function. -Finally, install and start the service: +#### Example: ```powershell -nssm install $name $exe $arg -nssm start $name +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. -To stop (or remove) the service afterwards, you can use the following: +--- -```powershell -nssm stop $name -nssm remove $name confirm -``` +## Unregistering a Service -#### Linux (systemd): +When you no longer need a service, unregister it with the `Unregister-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 +Unregister-PodeService -Name "HelloService" -Force ``` -```bash -[Unit] -Description=Pode Web Server -After=network.target +This forcefully stops and removes the service, returning `$true` if successful. -[Service] -ExecStart=/usr/bin/pwsh -c /usr/src/pode/server.ps1 -nop -ep Bypass -Restart=always +--- -[Install] -WantedBy=multi-user.target -Alias=pode-server.service -``` +## Alternative Methods for Windows and Linux -Finally, start the service: +If the Pode functions are unavailable or you prefer manual management, you can use traditional methods to configure Pode as a service. -```powershell -sudo systemctl start pode-server -``` +### Windows (NSSM) -!!! info - You can now navigate to your server, ie: `http://localhost:8080`. +To use NSSM for Pode as a Windows service: -To stop the service afterwards, you can use the following: +1. Install NSSM using Chocolatey: -```powershell -sudo systemctl stop pode-server -``` -### Using Ports Below 1024 + ```powershell + choco install nssm -y + ``` -#### Introduction +2. Configure the service: -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. + ```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 + ``` -#### Using a Reverse Proxy +3. Stop or remove the service: -A reverse proxy like Nginx can listen on the privileged port and forward requests to your application running on an unprivileged port. + ```powershell + nssm stop $name + nssm remove $name confirm + ``` -**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/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 06d34511b..678e86c6f 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -30,6 +30,34 @@ public class PodeMonitor public int StartMaxRetryCount { get; } // Maximum retries to start the process public int StartRetryDelayMs { get; } // Delay between retries in milliseconds + // Thread-safe variable to track the service state + private volatile bool _suspended; // Volatile ensures the latest value is visible to all threads + + public bool Suspended + { + get => _suspended; // Safe to read from multiple threads + private set => _suspended = value; // Written by only one thread + } + + // Thread-safe variable to track the service state + private volatile bool _starting; // Volatile ensures the latest value is visible to all threads + + public bool Starting + { + get => _starting; // Safe to read from multiple threads + private set => _starting = value; // Written by only one thread + } + + // Thread-safe variable to track the service state + private volatile bool _running; // Volatile ensures the latest value is visible to all threads + + public bool Running + { + get => _running; // Safe to read from multiple threads + private set => _running = value; // Written by only one thread + } + + /// /// Initializes a new instance of the PodeMonitor class. /// @@ -88,7 +116,50 @@ public void StartPowerShellProcess() }; // Subscribe to output and error streams - _powerShellProcess.OutputDataReceived += (sender, args) => PodeMonitorLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); + //_powerShellProcess.OutputDataReceived += (sender, args) => PodeMonitorLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); + + // Subscribe to output and error streams + _powerShellProcess.OutputDataReceived += (sender, args) => + { + // Log the received message + if (!string.IsNullOrEmpty(args.Data)) + { + PodeMonitorLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); + + // Check if the message starts with "Service State:" + if (args.Data.StartsWith("Service State: ", StringComparison.OrdinalIgnoreCase)) + { + // Extract the state value and update the static variable + string state = args.Data.Substring("Service State: ".Length).Trim().ToLowerInvariant(); + + if (state == "running") + { + Suspended = false; + Starting = false; + Running = true; + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service state updated to: Running."); + } + else if (state == "suspended") + { + Suspended = true; + Starting = false; + Running = false; + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service state updated to: Suspended."); + } + else if (state == "starting") + { + Suspended = false; + Starting = true; + Running = false; + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service state updated to: Restarting."); + } + else + { + PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Unknown service state: {state}"); + } + } + } + }; _powerShellProcess.ErrorDataReceived += (sender, args) => PodeMonitorLogger.Log(LogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); // Start the process diff --git a/src/PodeMonitor/PodeMonitorLogger.cs b/src/PodeMonitor/PodeMonitorLogger.cs index 2a4e388b2..8ccb10f64 100644 --- a/src/PodeMonitor/PodeMonitorLogger.cs +++ b/src/PodeMonitor/PodeMonitorLogger.cs @@ -1,12 +1,9 @@ - using System; using System.IO; +using System.Text.RegularExpressions; namespace PodeMonitor { - using System; - using System.IO; - public enum LogLevel { DEBUG, // Detailed information for debugging purposes @@ -16,11 +13,14 @@ public enum LogLevel CRITICAL // Critical errors indicating severe failures } - public static class PodeMonitorLogger + public static partial class PodeMonitorLogger { - private static readonly object _logLock = new(); - private static string logFilePath = "PodeService.log"; // Default log file path - private static LogLevel minLogLevel = LogLevel.INFO; // Default minimum log level + private static readonly object _logLock = new(); // Ensures thread-safe writes + private static string _logFilePath = "PodeService.log"; // Default log file path + private static LogLevel _minLogLevel = LogLevel.INFO; // Default minimum log level + + [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. @@ -29,22 +29,25 @@ public static class PodeMonitorLogger /// Minimum log level to record. public static void Initialize(string filePath, LogLevel level) { - if (!string.IsNullOrWhiteSpace(filePath)) + try { - logFilePath = filePath; - } + // Update the log file path and minimum log level + if (!string.IsNullOrWhiteSpace(filePath)) + { + _logFilePath = filePath; + } - minLogLevel = level; + _minLogLevel = level; - try - { - // Create the log file if it doesn't exist - if (!File.Exists(logFilePath)) + // Ensure the log file exists + if (!File.Exists(_logFilePath)) { - using (File.Create(logFilePath)) { } + using (File.Create(_logFilePath)) { }; } - Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", logFilePath, minLogLevel); + // Log initialization success + Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, + "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", _logFilePath, _minLogLevel); } catch (Exception ex) { @@ -52,28 +55,39 @@ public static void Initialize(string filePath, LogLevel level) } } + /// + /// Logs a message to the log file with the specified log level and context. + /// + /// 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(LogLevel level, string context, int pid, string message = "", params object[] args) { - if (level < minLogLevel || string.IsNullOrEmpty(message)) + if (level < _minLogLevel || string.IsNullOrEmpty(message)) { - return; // Skip logging for levels below the minimum log level + return; // Skip logging for levels below the minimum log level or empty messages } try { - // Format the message with the provided arguments - var formattedMessage = string.Format(message, args); + // 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 in GMT/UTC + // Get the current time in ISO 8601 format (UTC) string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); - // Build the log entry + // Construct the log entry string logEntry = $"{timestamp} [PID:{pid}] [{level}] [{context}] {formattedMessage}"; - // Thread-safe write to log file + // Thread-safe log file write lock (_logLock) { - using StreamWriter writer = new(logFilePath, true); + using StreamWriter writer = new(_logFilePath, true); writer.WriteLine(logEntry); } } @@ -83,30 +97,32 @@ public static void Log(LogLevel level, string context, int pid, string message = } } - + /// + /// Logs an exception and an optional message to the log file. + /// + /// Log level. + /// Exception to log. + /// Optional message to include. + /// Optional arguments for formatting the message. public static void Log(LogLevel level, Exception exception, string message = null, params object[] args) { - if (level < minLogLevel) + if (level < _minLogLevel || (exception == null && string.IsNullOrEmpty(message))) { - return; // Skip logging for levels below the minimum log level - } - - if (exception == null && string.IsNullOrEmpty(message)) - { - return; // Nothing to log + 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 in GMT/UTC + // 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 - var logMessage = string.Empty; - + string logMessage = string.Empty; if (!string.IsNullOrEmpty(message)) { - logMessage = string.Format(message, args); + // Sanitize the message to remove ANSI escape codes + string sanitizedMessage = AnsiRegex().Replace(message, string.Empty); + logMessage = string.Format(sanitizedMessage, args); } // Add exception details @@ -130,13 +146,13 @@ public static void Log(LogLevel level, Exception exception, string message = nul // Get the current process ID int pid = Environment.ProcessId; - // Build the log entry + // Construct the log entry string logEntry = $"{timestamp} [PID:{pid}] [{level}] [PodeMonitor] {logMessage}"; - // Thread-safe write to log file + // Thread-safe log file write lock (_logLock) { - using StreamWriter writer = new(logFilePath, true); + using StreamWriter writer = new(_logFilePath, true); writer.WriteLine(logEntry); } } @@ -146,4 +162,4 @@ public static void Log(LogLevel level, Exception exception, string message = nul } } } -} \ No newline at end of file +} diff --git a/src/PodeMonitor/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs index 36d3bccbf..0bd068fd7 100644 --- a/src/PodeMonitor/PodeMonitorWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -22,11 +22,9 @@ public sealed class PodeMonitorWorker : BackgroundService, IPausableHostedServic // Delay in milliseconds to prevent rapid consecutive operations private readonly int _delayMs = 5000; - private bool _terminating=false; + private bool _terminating = false; - private bool _suspended=false; - private bool _running=false; /// /// Initializes a new instance of the PodeMonitorWorker class. /// @@ -57,7 +55,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // Start the Pode PowerShell process _pwshMonitor.StartPowerShellProcess(); - _running=true; } catch (Exception ex) { @@ -101,21 +98,23 @@ public override async Task StopAsync(CancellationToken stoppingToken) /// public void Shutdown() { - if((! _terminating )&& _running){ + if ((!_terminating) && (_pwshMonitor.Running || _pwshMonitor.Suspended)) + { - _terminating=true; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); + _terminating = true; + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); - try - { - _pwshMonitor.StopPowerShellProcess(); // Stop the process - _running=false; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Stop message sent via pipe at: {0}", DateTimeOffset.Now); + try + { + _pwshMonitor.StopPowerShellProcess(); // Stop the process + + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Stop message sent via pipe at: {0}", DateTimeOffset.Now); + } + catch (Exception ex) + { + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); + } } - catch (Exception ex) - { - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); - }} } /// @@ -123,14 +122,26 @@ public void Shutdown() /// public void Restart() { - if((! _terminating )&& _running){ - + if ((!_terminating) && _pwshMonitor.Running) + { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); - try { _pwshMonitor.RestartPowerShellProcess(); // Restart the process PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + + var retryCount = 0; // Reset retry count on success + while (_pwshMonitor.Starting) + { + if (retryCount >= 100) + { + PodeMonitorLogger.Log(LogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + break; + } + + // Delay before retrying + Thread.Sleep(200); + } } catch (Exception ex) { @@ -144,22 +155,34 @@ public void Restart() /// public void OnPause() { - if((! _terminating )&& _running){ - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); - - try + if ((!_terminating) && _pwshMonitor.Running) { - _pwshMonitor.SuspendPowerShellProcess(); // Send pause command to the process -_suspended=true; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); - AddOperationDelay("Pause"); // Delay to ensure stability - } - catch (Exception ex) - { - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); + try + { + _pwshMonitor.SuspendPowerShellProcess(); // Send pause command to the process + + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + var retryCount = 0; // Reset retry count on success + while (!_pwshMonitor.Suspended) + { + if (retryCount >= 100) + { + PodeMonitorLogger.Log(LogLevel.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(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); + } } - } } /// @@ -167,17 +190,30 @@ public void OnPause() /// public void OnContinue() { - if((! _terminating )&& _suspended){ + if ((!_terminating) && _pwshMonitor.Suspended) + { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); try { _pwshMonitor.ResumePowerShellProcess(); // Send resume command to the process -_suspended=false; PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); + var retryCount = 0; // Reset retry count on success + while (_pwshMonitor.Suspended) + { + if (retryCount >= 100) + { + PodeMonitorLogger.Log(LogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + break; + } + + // Delay before retrying + Thread.Sleep(200); + } + - AddOperationDelay("Resume"); // Delay to ensure stability + // AddOperationDelay("Resume"); // Delay to ensure stability } catch (Exception ex) { diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 766be7d96..2a74d9d48 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -328,13 +328,13 @@ function New-PodeContext { # routes for pages and api $ctx.Server.Routes = [ordered]@{ -# common methods + # common methods 'get' = [ordered]@{} 'post' = [ordered]@{} 'put' = [ordered]@{} 'patch' = [ordered]@{} 'delete' = [ordered]@{} -# other methods + # other methods 'connect' = [ordered]@{} 'head' = [ordered]@{} 'merge' = [ordered]@{} @@ -445,7 +445,7 @@ function New-PodeContext { Tasks = $null Files = $null Timers = $null - Service =$null + Service = $null } # threading locks, etc. diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 3c0d78547..3ef4ea8e5 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -66,8 +66,6 @@ function Start-PodeInternalServer { # start runspace for loggers Start-PodeLoggingRunspace - Start-PodeServiceHearthbeat - # start runspace for schedules Start-PodeScheduleRunspace @@ -209,6 +207,8 @@ function Start-PodeInternalServer { } } } + # Start Service Monitor + Start-PodeServiceHearthbeat } catch { throw @@ -334,8 +334,8 @@ function Restart-PodeInternalServer { Close-PodeDisposable -Disposable $PodeContext.Tokens.Cancellation $PodeContext.Tokens.Cancellation = [System.Threading.CancellationTokenSource]::new() - Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart - $PodeContext.Tokens.Restart = [System.Threading.CancellationTokenSource]::new() + # Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart + # $PodeContext.Tokens.Restart = [System.Threading.CancellationTokenSource]::new() # reload the configuration $PodeContext.Server.Configuration = Open-PodeConfiguration -Context $PodeContext @@ -346,6 +346,11 @@ function Restart-PodeInternalServer { # restart the server $PodeContext.Metrics.Server.RestartCount++ Start-PodeInternalServer + + # recreate the session tokens + + Close-PodeDisposable -Disposable $PodeContext.Tokens.Restart + $PodeContext.Tokens.Restart = [System.Threading.CancellationTokenSource]::new() } catch { $_ | Write-PodeErrorLog diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index ee1560d5c..03ea403a4 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -57,9 +57,11 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { - + $serviceState='running' while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + Write-PodeHost -Message "Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force + Write-PodeHost -Message "Service State: $serviceState" -Force Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force Write-PodeHost -Message "Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force Write-PodeHost -Message "Total Number of Restart: $(Get-PodeServerRestartCount)" -Force @@ -105,7 +107,9 @@ function Start-PodeServiceHearthbeat { Write-PodeHost -Message 'Server requested restart. Restarting Pode ...' -Force Restart-PodeServer # Restart Pode server Start-Sleep 1 + $serviceState = 'starting' Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force + Write-PodeHost -Message "Service State: $serviceState" -Force return # Exit the loop } @@ -114,6 +118,7 @@ function Start-PodeServiceHearthbeat { # Process 'suspend' message Write-PodeHost -Message 'Server requested suspend. Suspending Pode ...' -Force Start-Sleep 5 + $serviceState = 'suspended' #Suspend-PodeServer # Suspend Pode server # return # Exit the loop } @@ -122,6 +127,7 @@ function Start-PodeServiceHearthbeat { # Process 'resume' message Write-PodeHost -Message 'Server requested resume. Resuming Pode ...' -Force Start-Sleep 5 + $serviceState = 'running' #Resume-PodeServer # Resume Pode server # return # Exit the loop } @@ -136,9 +142,13 @@ function Start-PodeServiceHearthbeat { throw $_ } finally { - $reader.Dispose() - $pipeStream.Dispose() # Always dispose of the pipe stream when done - Write-PodeHost -Message "Disposing Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force + if ($reader) { + $reader.Dispose() + } + if ( $pipeStream) { + $pipeStream.Dispose() # Always dispose of the pipe stream when done + Write-PodeHost -Message "Disposing Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force + } } } From 5ee22257dbfbbdb086e2aad661c677d2153516cc Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 21 Nov 2024 10:14:29 -0800 Subject: [PATCH 46/93] improvements --- src/PodeMonitor/PodeMonitor.cs | 281 +++++++++++++++++---------------- src/Private/Service.ps1 | 1 + 2 files changed, 148 insertions(+), 134 deletions(-) diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 678e86c6f..40045dd5b 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -7,64 +7,68 @@ namespace PodeMonitor { /// - /// The PodeMonitor class monitors and controls the execution of a Pode PowerShell process. - /// It communicates with the Pode process using named pipes. + /// Enum representing possible states of the Pode service. + /// + public enum ServiceState + { + Unknown, // State is unknown + Running, // Service is running + Suspended, // Service is suspended + Starting // Service is starting + } + + /// + /// 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 communication + 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 to pass to the script - private readonly string _pwshPath; // Path to the PowerShell executable - private readonly bool _quiet; // Whether the process runs in quiet mode - private readonly bool _disableTermination; // Whether termination is disabled + 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 bool _quiet; // Indicates whether the process runs in quiet mode + private readonly bool _disableTermination; // Indicates whether termination is disabled private readonly int _shutdownWaitTimeMs; // Timeout for shutting down the process - private readonly string _pipeName; // Name of the pipe for interprocess communication - - private DateTime _lastLogTime; // Last log timestamp - - public int StartMaxRetryCount { get; } // Maximum retries to start the process - public int StartRetryDelayMs { get; } // Delay between retries in milliseconds - - // Thread-safe variable to track the service state - private volatile bool _suspended; // Volatile ensures the latest value is visible to all threads + private readonly string _pipeName; // Name of the named pipe for communication - public bool Suspended - { - get => _suspended; // Safe to read from multiple threads - private set => _suspended = value; // Written by only one thread - } + private DateTime _lastLogTime; // Tracks the last time the process logged activity - // Thread-safe variable to track the service state - private volatile bool _starting; // Volatile ensures the latest value is visible to all threads + public int StartMaxRetryCount { get; } // Maximum number of retries for starting the process + public int StartRetryDelayMs { get; } // Delay between retries in milliseconds - public bool Starting - { - get => _starting; // Safe to read from multiple threads - private set => _starting = value; // Written by only one thread - } + // Volatile variables ensure thread-safe visibility for all threads + private volatile bool _suspended; + private volatile bool _starting; + private volatile bool _running; - // Thread-safe variable to track the service state - private volatile bool _running; // Volatile ensures the latest value is visible to all threads + /// + /// Indicates whether the service is suspended. + /// + public bool Suspended => _suspended; - public bool Running - { - get => _running; // Safe to read from multiple threads - private set => _running = value; // Written by only one thread - } + /// + /// Indicates whether the service is starting. + /// + public bool Starting => _starting; + /// + /// Indicates whether the service is running. + /// + public bool Running => _running; /// - /// Initializes a new instance of the PodeMonitor class. + /// Initializes a new instance of the class with the specified configuration options. /// - /// The configuration options for the PodeMonitorWorker. + /// Configuration options for the PodeMonitor. public PodeMonitor(PodeMonitorWorkerOptions options) { - // Initialize configuration properties from options + // Initialize configuration properties _scriptPath = options.ScriptPath; _pwshPath = options.PwshPath; _parameterString = options.ParameterString; @@ -74,23 +78,20 @@ public PodeMonitor(PodeMonitorWorkerOptions options) StartMaxRetryCount = options.StartMaxRetryCount; StartRetryDelayMs = options.StartRetryDelayMs; - // Generate a unique pipe name for communication + // Generate a unique pipe name _pipeName = PipeNameGenerator.GeneratePipeName(); PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName}"); } /// - /// Starts the Pode PowerShell process. - /// If the process is already running, logs its status. + /// Starts the Pode PowerShell process. If the process is already running, logs its status. /// public void StartPowerShellProcess() { - lock (_syncLock) // Ensure thread-safe access + lock (_syncLock) { - // Check if the process is already running if (_powerShellProcess != null && !_powerShellProcess.HasExited) { - // Log if the process is alive and log threshold is met if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is Alive."); @@ -108,72 +109,39 @@ public void StartPowerShellProcess() { FileName = _pwshPath, Arguments = BuildCommand(), - RedirectStandardOutput = true, // Redirect standard output - RedirectStandardError = true, // Redirect standard error - UseShellExecute = false, // Do not use shell execution - CreateNoWindow = true // Prevent creating a new window + 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 output and error streams - //_powerShellProcess.OutputDataReceived += (sender, args) => PodeMonitorLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); - - // Subscribe to output and error streams + // Subscribe to the output stream for logging and state parsing _powerShellProcess.OutputDataReceived += (sender, args) => { - // Log the received message if (!string.IsNullOrEmpty(args.Data)) { PodeMonitorLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); - - // Check if the message starts with "Service State:" - if (args.Data.StartsWith("Service State: ", StringComparison.OrdinalIgnoreCase)) - { - // Extract the state value and update the static variable - string state = args.Data.Substring("Service State: ".Length).Trim().ToLowerInvariant(); - - if (state == "running") - { - Suspended = false; - Starting = false; - Running = true; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service state updated to: Running."); - } - else if (state == "suspended") - { - Suspended = true; - Starting = false; - Running = false; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service state updated to: Suspended."); - } - else if (state == "starting") - { - Suspended = false; - Starting = true; - Running = false; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service state updated to: Restarting."); - } - else - { - PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Unknown service state: {state}"); - } - } + ParseServiceState(args.Data); } }; - _powerShellProcess.ErrorDataReceived += (sender, args) => PodeMonitorLogger.Log(LogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); - // Start the process + // Subscribe to the error stream for logging errors + _powerShellProcess.ErrorDataReceived += (sender, args) => + { + PodeMonitorLogger.Log(LogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); + }; + + // Start the process and begin reading the output/error streams _powerShellProcess.Start(); _powerShellProcess.BeginOutputReadLine(); _powerShellProcess.BeginErrorReadLine(); - // Log the process start time _lastLogTime = DateTime.Now; PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process started successfully."); } catch (Exception ex) { - // Log any errors during process start PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to start Pode process: {ex.Message}"); PodeMonitorLogger.Log(LogLevel.DEBUG, ex); } @@ -181,12 +149,11 @@ public void StartPowerShellProcess() } /// - /// Stops the Pode PowerShell process gracefully. - /// If the process does not terminate gracefully, it will be forcefully terminated. + /// Stops the Pode PowerShell process gracefully. If it does not terminate, it is forcefully killed. /// public void StopPowerShellProcess() { - lock (_syncLock) // Ensure thread-safe access + lock (_syncLock) { if (_powerShellProcess == null || _powerShellProcess.HasExited) { @@ -196,18 +163,15 @@ public void StopPowerShellProcess() try { - if (InitializePipeClient()) // Ensure pipe client is initialized + if (InitializePipeClientWithRetry()) { - // Send shutdown message and wait for process exit SendPipeMessage("shutdown"); - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); - // If process does not exit gracefully, forcefully terminate if (!_powerShellProcess.HasExited) { - PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully, killing process."); + PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully. Killing process."); _powerShellProcess.Kill(); } @@ -216,13 +180,11 @@ public void StopPowerShellProcess() } catch (Exception ex) { - // Log errors during stop process PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error stopping Pode process: {ex.Message}"); PodeMonitorLogger.Log(LogLevel.DEBUG, ex); } finally { - // Clean up resources CleanupResources(); } } @@ -231,26 +193,17 @@ public void StopPowerShellProcess() /// /// Sends a suspend command to the Pode process via named pipe. /// - public void SuspendPowerShellProcess() - { - ExecutePipeCommand("suspend"); - } + public void SuspendPowerShellProcess() => ExecutePipeCommand("suspend"); /// /// Sends a resume command to the Pode process via named pipe. /// - public void ResumePowerShellProcess() - { - ExecutePipeCommand("resume"); - } + public void ResumePowerShellProcess() => ExecutePipeCommand("resume"); /// /// Sends a restart command to the Pode process via named pipe. /// - public void RestartPowerShellProcess() - { - ExecutePipeCommand("restart"); - } + public void RestartPowerShellProcess() => ExecutePipeCommand("restart"); /// /// Executes a command by sending it to the Pode process via named pipe. @@ -258,11 +211,11 @@ public void RestartPowerShellProcess() /// The command to execute (e.g., "suspend", "resume", "restart"). private void ExecutePipeCommand(string command) { - lock (_syncLock) // Ensure thread-safe access + lock (_syncLock) { try { - if (InitializePipeClient()) // Ensure pipe client is initialized + if (InitializePipeClientWithRetry()) { SendPipeMessage(command); PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"{command.ToUpper()} command sent to Pode process."); @@ -270,18 +223,60 @@ private void ExecutePipeCommand(string command) } catch (Exception ex) { - // Log errors during command execution PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error executing {command} command: {ex.Message}"); PodeMonitorLogger.Log(LogLevel.DEBUG, ex); } finally { - // Clean up pipe client after sending the command 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.Substring("Service State: ".Length).Trim().ToLowerInvariant(); + + switch (state) + { + case "running": + UpdateServiceState(ServiceState.Running); + break; + case "suspended": + UpdateServiceState(ServiceState.Suspended); + break; + case "starting": + UpdateServiceState(ServiceState.Starting); + break; + default: + PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Unknown service state: {state}"); + UpdateServiceState(ServiceState.Unknown); + break; + } + } + } + + /// + /// Updates the internal state variables based on the provided service state. + /// + /// The new service state. + private void UpdateServiceState(ServiceState state) + { + _suspended = state == ServiceState.Suspended; + _starting = state == ServiceState.Starting; + _running = state == ServiceState.Running; + + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state updated to: {state}"); + } + /// /// Builds the PowerShell command to execute the Pode process. /// @@ -293,23 +288,41 @@ private string BuildCommand() } /// - /// Initializes the named pipe client for communication with the Pode process. + /// Initializes the named pipe client with a retry mechanism. /// - /// True if the pipe client is successfully initialized and connected; otherwise, false. - private bool InitializePipeClient() + /// The maximum number of retries for connection. + /// True if the pipe client is successfully connected; otherwise, false. + private bool InitializePipeClientWithRetry(int maxRetries = 3) { - if (_pipeClient == null) - { - _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); - } + int attempts = 0; - if (!_pipeClient.IsConnected) + while (attempts < maxRetries) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Connecting to pipe server..."); - _pipeClient.Connect(10000); // Connect with a timeout of 10 seconds + try + { + if (_pipeClient == null) + { + _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); + } + + if (!_pipeClient.IsConnected) + { + PodeMonitorLogger.Log(LogLevel.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(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Pipe connection attempt {attempts + 1} failed: {ex.Message}"); + } + + attempts++; + Thread.Sleep(1000); } - return _pipeClient.IsConnected; + return false; } /// @@ -321,7 +334,7 @@ private void SendPipeMessage(string message) try { using var writer = new StreamWriter(_pipeClient) { AutoFlush = true }; - writer.WriteLine(message); // Write the message to the pipe + writer.WriteLine(message); } catch (Exception ex) { @@ -331,7 +344,7 @@ private void SendPipeMessage(string message) } /// - /// Waits for the Pode process to exit within the specified timeout period. + /// Waits for the Pode process to exit within the specified timeout. /// /// The timeout period in milliseconds. private void WaitForProcessExit(int timeout) @@ -339,13 +352,13 @@ private void WaitForProcessExit(int timeout) int waited = 0; while (!_powerShellProcess.HasExited && waited < timeout) { - Thread.Sleep(200); // Check every 200ms + Thread.Sleep(200); waited += 200; } } /// - /// Cleans up resources associated with the Pode process and named pipe client. + /// Cleans up resources associated with the Pode process and the pipe client. /// private void CleanupResources() { @@ -364,4 +377,4 @@ private void CleanupPipeClient() _pipeClient = null; } } -} +} \ No newline at end of file diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 03ea403a4..85f4dc085 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -89,6 +89,7 @@ function Start-PodeServiceHearthbeat { if ( $PodeContext.Tokens.Cancellation.IsCancellationRequested) { return } + if ($message) { Write-PodeHost -Message "Received message: $message" -Force From 3cdfb0d7cd6c8ddf66ee9b541779663e2c5955be Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 21 Nov 2024 21:16:04 -0800 Subject: [PATCH 47/93] windows fixes --- src/PodeMonitor/IPausableHostedService.cs | 2 + src/PodeMonitor/PodeMonitor.cs | 102 +++++++++++++++++----- src/PodeMonitor/PodeMonitorWorker.cs | 31 +++---- src/Private/Service.ps1 | 8 +- src/Public/Service.ps1 | 6 +- 5 files changed, 101 insertions(+), 48 deletions(-) diff --git a/src/PodeMonitor/IPausableHostedService.cs b/src/PodeMonitor/IPausableHostedService.cs index 5c526f4db..0395b23cf 100644 --- a/src/PodeMonitor/IPausableHostedService.cs +++ b/src/PodeMonitor/IPausableHostedService.cs @@ -20,5 +20,7 @@ public interface IPausableHostedService void Restart(); + + public ServiceState State { get; } } } diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 40045dd5b..cfdb3c62c 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Diagnostics; using System.IO; using System.IO.Pipes; @@ -36,31 +37,22 @@ public class PodeMonitor private readonly bool _disableTermination; // Indicates whether termination is disabled private readonly int _shutdownWaitTimeMs; // Timeout for shutting down the process private readonly string _pipeName; // Name of the named pipe for communication + 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 - // Volatile variables ensure thread-safe visibility for all threads - private volatile bool _suspended; - private volatile bool _starting; - private volatile bool _running; + private volatile ServiceState _state; + + + public ServiceState State { get => _state; set => _state = value; } + + public bool DisableTermination { get => _disableTermination; } - /// - /// Indicates whether the service is suspended. - /// - public bool Suspended => _suspended; - /// - /// Indicates whether the service is starting. - /// - public bool Starting => _starting; - /// - /// Indicates whether the service is running. - /// - public bool Running => _running; /// /// Initializes a new instance of the class with the specified configuration options. @@ -81,6 +73,15 @@ public PodeMonitor(PodeMonitorWorkerOptions options) // Generate a unique pipe name _pipeName = PipeNameGenerator.GeneratePipeName(); PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName}"); + // Define the state file path only for Linux/macOS + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + // Define the state file path (default to /var/tmp for Linux/macOS) + _stateFilePath = Path.Combine("/var/tmp", $"PodeService_{Environment.ProcessId}.state"); + + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName} and state file: {_stateFilePath}"); + } + } /// @@ -155,7 +156,7 @@ public void StopPowerShellProcess() { lock (_syncLock) { - if (_powerShellProcess == null || _powerShellProcess.HasExited) + if (_powerShellProcess == null || (_powerShellProcess.HasExited && Process.GetProcessById(_powerShellProcess.Id) == null)) { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is not running."); return; @@ -169,8 +170,10 @@ public void StopPowerShellProcess() PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); - if (!_powerShellProcess.HasExited) + if (_powerShellProcess != null || !_powerShellProcess.HasExited || Process.GetProcessById(_powerShellProcess.Id) != null) { + // var p=Process.GetProcessById(_powerShellProcess.Id); + // p.Kill(); PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully. Killing process."); _powerShellProcess.Kill(); } @@ -205,6 +208,8 @@ public void StopPowerShellProcess() /// public void RestartPowerShellProcess() => ExecutePipeCommand("restart"); + + /// /// Executes a command by sending it to the Pode process via named pipe. /// @@ -243,7 +248,7 @@ private void ParseServiceState(string output) if (output.StartsWith("Service State: ", StringComparison.OrdinalIgnoreCase)) { - string state = output.Substring("Service State: ".Length).Trim().ToLowerInvariant(); + string state = output["Service State: ".Length..].Trim().ToLowerInvariant(); switch (state) { @@ -270,11 +275,14 @@ private void ParseServiceState(string output) /// The new service state. private void UpdateServiceState(ServiceState state) { - _suspended = state == ServiceState.Suspended; - _starting = state == ServiceState.Starting; - _running = state == ServiceState.Running; - + _state = state; PodeMonitorLogger.Log(LogLevel.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); + } } /// @@ -357,6 +365,49 @@ private void WaitForProcessExit(int timeout) } } + + /// + /// Writes the current service state to the state file. + /// + /// The service state to write. + private void WriteServiceStateToFile(ServiceState state) + { + lock (_syncLock) // Ensure thread-safe access + { + try + { + File.WriteAllText(_stateFilePath, state.ToString().ToLowerInvariant()); + PodeMonitorLogger.Log(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Service state written to file: {_stateFilePath}"); + } + catch (Exception ex) + { + PodeMonitorLogger.Log(LogLevel.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(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state file deleted: {_stateFilePath}"); + } + } + catch (Exception ex) + { + PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to delete service state file: {ex.Message}"); + } + } + } + /// /// Cleans up resources associated with the Pode process and the pipe client. /// @@ -366,6 +417,11 @@ private void CleanupResources() _powerShellProcess = null; CleanupPipeClient(); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + DeleteServiceStateFile(); + } } /// diff --git a/src/PodeMonitor/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs index 0bd068fd7..a25a0ab3f 100644 --- a/src/PodeMonitor/PodeMonitorWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -24,6 +24,8 @@ public sealed class PodeMonitorWorker : BackgroundService, IPausableHostedServic private bool _terminating = false; + public ServiceState State => _pwshMonitor.State; + /// /// Initializes a new instance of the PodeMonitorWorker class. @@ -47,7 +49,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) PodeMonitorLogger.Log(LogLevel.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) + while (!stoppingToken.IsCancellationRequested && !_terminating) { try { @@ -98,7 +100,7 @@ public override async Task StopAsync(CancellationToken stoppingToken) /// public void Shutdown() { - if ((!_terminating) && (_pwshMonitor.Running || _pwshMonitor.Suspended)) + if ((!_terminating) && (_pwshMonitor.State == ServiceState.Running || _pwshMonitor.State == ServiceState.Suspended)) { _terminating = true; @@ -122,7 +124,7 @@ public void Shutdown() /// public void Restart() { - if ((!_terminating) && _pwshMonitor.Running) + if ((!_terminating) && _pwshMonitor.State == ServiceState.Running) { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); try @@ -130,22 +132,11 @@ public void Restart() _pwshMonitor.RestartPowerShellProcess(); // Restart the process PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); - var retryCount = 0; // Reset retry count on success - while (_pwshMonitor.Starting) - { - if (retryCount >= 100) - { - PodeMonitorLogger.Log(LogLevel.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(LogLevel.ERROR, ex, "Error during restart: {0}", ex.Message); + PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); } } } @@ -155,7 +146,7 @@ public void Restart() /// public void OnPause() { - if ((!_terminating) && _pwshMonitor.Running) + if ((!_terminating) && _pwshMonitor.State == ServiceState.Running) { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); @@ -165,7 +156,7 @@ public void OnPause() PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); var retryCount = 0; // Reset retry count on success - while (!_pwshMonitor.Suspended) + while (_pwshMonitor.State != ServiceState.Suspended) { if (retryCount >= 100) { @@ -190,7 +181,7 @@ public void OnPause() /// public void OnContinue() { - if ((!_terminating) && _pwshMonitor.Suspended) + if ((!_terminating) && _pwshMonitor.State == ServiceState.Suspended) { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); @@ -200,7 +191,7 @@ public void OnContinue() PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); var retryCount = 0; // Reset retry count on success - while (_pwshMonitor.Suspended) + while (_pwshMonitor.State == ServiceState.Suspended) { if (retryCount >= 100) { diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 85f4dc085..1fe7b74ee 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -106,11 +106,13 @@ function Start-PodeServiceHearthbeat { 'restart' { # Process 'restart' message Write-PodeHost -Message 'Server requested restart. Restarting Pode ...' -Force - Restart-PodeServer # Restart Pode server - Start-Sleep 1 + $serviceState = 'starting' - Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force Write-PodeHost -Message "Service State: $serviceState" -Force + Start-Sleep 1 + Restart-PodeServer # Restart Pode server + Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force + Start-Sleep 1 return # Exit the loop } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index b44502c85..3b14bf73c 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -300,7 +300,7 @@ function Start-PodeService { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is already running - if ($service.Status -ne 'Running') { + if ($service.Status -eq 'Stopped') { $null = Invoke-PodeWinElevatedCommand -Command 'Start-Service' -Arguments "-Name '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue @@ -313,7 +313,8 @@ function Start-PodeService { } else { # Log service is already running - Write-Verbose -Message "Service '$Name' is already running." + Write-Verbose -Message "Service '$Name' is $($service.Status)." + return ($service.Status -eq 'Running') } } else { @@ -1107,6 +1108,7 @@ function Restart-PodeService { if ($service.Status -eq 'Running' -or $service.Status -eq 'Paused') { Write-Verbose -Message "Sending restart (128) signal to service '$Name'." $null = Invoke-PodeWinElevatedCommand -Command 'sc control' -Arguments "'$Name' 128" + Start-Sleep 5 $service = Get-Service -Name $Name -ErrorAction SilentlyContinue Write-Verbose -Message "Service '$Name' restart signal sent successfully." } From b0bd2041825943a8c73c66cb9e1ddd8992e787a5 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 22 Nov 2024 06:30:30 -0800 Subject: [PATCH 48/93] Update Helpers.ps1 --- src/Private/Helpers.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index e4093f281..f14997a4e 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3807,7 +3807,7 @@ function Test-PodeAdminPrivilege { # Retrieve the current Windows identity and token $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) - + if ($null -eq $principal) { return $false } @@ -3818,7 +3818,7 @@ function Test-PodeAdminPrivilege { } # Check if the token is elevated - if ($identity.IsSystem || $identity.IsAuthenticated -and $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + if ($identity.IsSystem -or $identity.IsAuthenticated -and $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { return $true } From 697cd8b8988d7a97eeea8e1b397c0540a7960e9f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 22 Nov 2024 08:34:00 -0800 Subject: [PATCH 49/93] adding tests --- docs/Hosting/RunAsService.md | 8 +- src/Private/Service.ps1 | 16 --- src/Public/Service.ps1 | 39 +++--- tests/integration/Service.Tests.ps1 | 70 ++++++++++ tests/unit/Service.Tests.ps1 | 204 ++++++++++++++++++++++++++++ 5 files changed, 299 insertions(+), 38 deletions(-) create mode 100644 tests/integration/Service.Tests.ps1 create mode 100644 tests/unit/Service.Tests.ps1 diff --git a/docs/Hosting/RunAsService.md b/docs/Hosting/RunAsService.md index e9525c611..7a33db3d8 100644 --- a/docs/Hosting/RunAsService.md +++ b/docs/Hosting/RunAsService.md @@ -48,11 +48,11 @@ The `Register-PodeService` function offers several parameters to customize your - **`-StartRetryDelayMs`** *(int)*: Delay in milliseconds between retry attempts to start the PowerShell process. Defaults to `5,000 ms`. -- **`-UserName`** *(string)*: - Specifies the username under which the service will run. Defaults to the current user. +- **`-WindowsUser`** *(string)*: + Specifies the username under which the service will run. Defaults to the current user (Windows only). -- **`-CreateUser`** *(switch)*: - Creates the user if it does not exist (Linux only). +- **`-LinuxUser`** *(string)*: + Specifies the username under which the service will run. Defaults to the current user (Linux Only). - **`-Start`** *(switch)*: Starts the service immediately after registration. diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 1fe7b74ee..245b0e1bb 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -339,9 +339,6 @@ function Register-PodeMacService { .PARAMETER Group The group under which the service will run. Defaults to the same as the `User` parameter. -.PARAMETER CreateUser - A switch create the user if it does not exist. - .PARAMETER OsArchitecture The architecture of the operating system (e.g., `x64`, `arm64`). Used to locate the appropriate binary. @@ -393,9 +390,6 @@ function Register-PodeLinuxService { [switch] $Start, - [switch] - $CreateUser, - [string] $OsArchitecture ) @@ -439,16 +433,6 @@ WantedBy=multi-user.target Remove-Item -path $tempFile -ErrorAction SilentlyContinue - # Create user if needed - if ($CreateUser.IsPresent) { - # Run the id command to check if the user exists - id $User 2>&1 - if ($LASTEXITCODE -ne 0) { - # Create the user if it doesn't exist - sudo useradd -r -s /bin/false $User - } - } - # Enable the service and check if it fails try { if (!(Enable-PodeLinuxService -Name $nameService)) { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 3b14bf73c..3c32d2d9c 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -34,11 +34,11 @@ .PARAMETER StartRetryDelayMs The delay (in milliseconds) between retry attempts to start the PowerShell process. Default is 5,000 milliseconds (5 seconds). -.PARAMETER UserName - Specifies the username under which the service will run by default is the current user. +.PARAMETER WindowsUser + Specifies the username under which the service will run by default is the current user (Windows only). -.PARAMETER CreateUser - A switch create the user if it does not exist (Linux Only). +.PARAMETER LinuxUser + Specifies the username under which the service will run by default is the current user (Linux Only). .PARAMETER Start A switch to start the service immediately after registration. @@ -101,10 +101,10 @@ function Register-PodeService { $StartRetryDelayMs = 5000, [string] - $UserName, + $WindowsUser, - [switch] - $CreateUser, + [string] + $LinuxUser, [switch] $Start, @@ -154,21 +154,25 @@ function Register-PodeService { if (! (Test-Path -Path $SettingsPath -PathType Container)) { $null = New-Item -Path $settingsPath -ItemType Directory } - - if ([string]::IsNullOrEmpty($UserName)) { - if ($IsWindows) { + if ($IsWindows) { + if ([string]::IsNullOrEmpty($WindowsUser)) { if ( ($null -ne $Password)) { $UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name } } else { - $UserName = [System.Environment]::UserName + $UserName = $WindowsUser + if ( ($null -eq $Password)) { + throw ($Podelocale.passwordRequiredForServiceUserException -f $UserName) + } } - } - else { - if ($IsWindows -and ($null -eq $Password)) { - throw ($Podelocale.passwordRequiredForServiceUserException -f $UserName) + elseif ($IsLinux) { + if ([string]::IsNullOrEmpty($LinuxUser)) { + $UserName = [System.Environment]::UserName + } + else { + $UserName = $LinuxUser } } @@ -223,7 +227,6 @@ function Register-PodeService { User = $User Group = $Group Start = $Start - CreateUser = $CreateUser OsArchitecture = "linux-$osArchitecture" } $operation = Register-PodeLinuxService @param @@ -482,7 +485,7 @@ function Stop-PodeService { # Check if the service is active if ((Test-PodeMacOsServiceIsActive $nameService)) { if ((Stop-PodeMacOsService $Name)) { - for($i=0;$i -lt 30; $i++){ + for ($i = 0; $i -lt 30; $i++) { if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$Name' stopped successfully." return $true @@ -633,7 +636,7 @@ function Resume-PodeService { } } elseif ($IsLinux -or $IsMacOS) { - return Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' + return Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' } } catch { diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 new file mode 100644 index 000000000..755db2f63 --- /dev/null +++ b/tests/integration/Service.Tests.ps1 @@ -0,0 +1,70 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + + + +Describe 'Service Lifecycle' { + + it 'register' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status.Status | Should -Be 'Stopped' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -Be 0 + } + + + it 'start' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start + start-sleep 5 + $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status.Status | Should -Be 'Running' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + $webRequest.Content | Should -Be 'Hello, Service!' + } + + it 'pause' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend + start-sleep 5 + # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status.Status | Should -Be 'Paused' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + # $webRequest | Should -BeNullOrEmpty + } + + it 'resume' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume + start-sleep 5 + $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status.Status | Should -Be 'Running' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + $webRequest.Content | Should -Be 'Hello, Service!' + } + it 'stop' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop + start-sleep 5 + + + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $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 'unregister' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Unregister + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status | Should -BeNullOrEmpty + } + +} \ No newline at end of file diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 new file mode 100644 index 000000000..5b69fc903 --- /dev/null +++ b/tests/unit/Service.Tests.ps1 @@ -0,0 +1,204 @@ +[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 { . $_ } + Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' +} + + +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 Get-Service + 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 + } + + 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-Service -MockWith { + if ($script:status -eq 'none') { + $script:status = 'Stopped' + } + else { + $script:status = 'Running' + } + [pscustomobject]@{ Name = 'TestService'; Status = $status } + } + Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { $null } + + # 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-Service -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) { + Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $true } + Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { $false } + Mock -CommandName Start-PodeLinuxService -MockWith { $true } + Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { $true } + + # Act + Start-PodeService -Name 'LinuxService' | Should -Be $true + + # Assert + Assert-MockCalled -CommandName Start-PodeLinuxService -Exactly 1 + } + + It 'Throws an error if the service is not registered' -Skip:(!$IsLinux) { + Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $false } + + { Start-PodeService -Name 'NonExistentService' } | Should -Throw + } + } + + Context 'On macOS platform' { + It 'Starts a stopped service successfully' -Skip:(!$IsMacOS) { + Mock -CommandName Test-PodeMacOsServiceIsRegistered -MockWith { $true } + Mock -CommandName Test-PodeMacOsServiceIsActive -MockWith { $false } + Mock -CommandName Start-PodeMacOsService -MockWith { $true } + Mock -CommandName Test-PodeMacOsServiceIsActive -MockWith { $true } + + # Act + Start-PodeService -Name 'MacService' | Should -Be $true + + # Assert + Assert-MockCalled -CommandName Start-PodeMacOsService -Exactly 1 + } + + It 'Throws an error if the service is not registered' -Skip:(!$IsMacOS) { + Mock -CommandName Test-PodeMacOsServiceIsRegistered -MockWith { $false } + + { Start-PodeService -Name 'NonExistentService' } | Should -Throw + } + } +} + From 770702e6cee7ca55c0ea83e4393fd76a34fb2a25 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Fri, 22 Nov 2024 17:04:55 -0800 Subject: [PATCH 50/93] Added stopping and fix suspended report on linux --- src/PodeMonitor/PodeMonitor.cs | 19 ++++++++++++---- src/Private/Helpers.ps1 | 2 +- src/Private/Service.ps1 | 2 ++ src/Public/Service.ps1 | 20 ++++++++++++----- tests/integration/Service.Tests.ps1 | 2 +- tests/unit/Service.Tests.ps1 | 35 +++++++++++++++++++++++------ 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index cfdb3c62c..1409c27e8 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -15,7 +15,8 @@ public enum ServiceState Unknown, // State is unknown Running, // Service is running Suspended, // Service is suspended - Starting // Service is starting + Starting, // Service is starting + Stopping // Service is stopping } /// @@ -76,8 +77,17 @@ public PodeMonitor(PodeMonitorWorkerOptions options) // Define the state file path only for Linux/macOS if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { + string stateDirectory = OperatingSystem.IsLinux() ? "/run/podemonitor" : + OperatingSystem.IsMacOS() ? "/private/var/run/podemonitor" : + throw new PlatformNotSupportedException("The current platform is not supported for setting the state directory."); + + if (!Directory.Exists(stateDirectory)) + { + Directory.CreateDirectory(stateDirectory); + } // Define the state file path (default to /var/tmp for Linux/macOS) - _stateFilePath = Path.Combine("/var/tmp", $"PodeService_{Environment.ProcessId}.state"); + _stateFilePath = Path.Combine(stateDirectory, $"{Environment.ProcessId}.state"); + PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName} and state file: {_stateFilePath}"); } @@ -172,8 +182,6 @@ public void StopPowerShellProcess() if (_powerShellProcess != null || !_powerShellProcess.HasExited || Process.GetProcessById(_powerShellProcess.Id) != null) { - // var p=Process.GetProcessById(_powerShellProcess.Id); - // p.Kill(); PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully. Killing process."); _powerShellProcess.Kill(); } @@ -261,6 +269,9 @@ private void ParseServiceState(string output) case "starting": UpdateServiceState(ServiceState.Starting); break; + case "stopping": + UpdateServiceState(ServiceState.Stopping); + break; default: PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Unknown service state: {state}"); UpdateServiceState(ServiceState.Unknown); diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index f14997a4e..dc46ae62b 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3846,7 +3846,7 @@ function Test-PodeAdminPrivilege { $groups = (groups $user) # macOS typically uses 'admin' group for sudo privileges - return ($groups -match '\bwheel\b' -or $groups -match '\badmin\b') + return ($groups -match '\bwheel\b' -or $groups -match '\badmin\b' -or $groups -match '\bsudo\b') } return $false } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 245b0e1bb..20b620831 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -97,6 +97,8 @@ function Start-PodeServiceHearthbeat { 'shutdown' { # Process 'shutdown' message Write-PodeHost -Message 'Server requested shutdown. Closing Pode ...' -Force + $serviceState = 'stopping' + Write-PodeHost -Message "Service State: $serviceState" -Force Close-PodeServer # Gracefully stop Pode server Start-Sleep 1 Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 3c32d2d9c..eee2d35c1 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -943,7 +943,7 @@ function Get-PodeService { switch ($service.State) { 'Running' { $status = 'Running' } 'Stopped' { $status = 'Stopped' } - 'Paused' { $status = 'Paused' } + 'Paused' { $status = 'Suspended' } 'StartPending' { $status = 'Starting' } 'StopPending' { $status = 'Stopping' } 'PausePending' { $status = 'Pausing' } @@ -971,10 +971,16 @@ function Get-PodeService { $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}') - $status = 'Running' + $stateFilePath = "/var/run/podemonitor/$servicePid.state" + if (Test-Path -Path $stateFilePath) { + $status = Get-Content -Path $stateFilePath + } } 'reloading' { $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') @@ -982,10 +988,10 @@ function Get-PodeService { } 'maintenance' { $servicePid = $(systemctl show -p MainPID $nameService | awk -F'=' '{print $2}') - $status = 'Paused' + $status = 'Suspended' } 'inactive' { - $status = 'Stopped' + $status = 'Suspended' } 'failed' { $status = 'Stopped' @@ -1027,11 +1033,15 @@ function Get-PodeService { $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + $stateFilePath = "/private/var/run/podemonitor/$servicePid.state" + if (Test-Path -Path $stateFilePath) { + $status = Get-Content -Path $stateFilePath + } # Check if the service has a PID entry if ($servicePid -ne 0) { return @{ Name = $Name - Status = 'Running' + Status = $status Pid = $servicePid Sudo = $sudo } diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 755db2f63..27497c191 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -31,7 +31,7 @@ Describe 'Service Lifecycle' { start-sleep 5 # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query - $status.Status | Should -Be 'Paused' + $status.Status | Should -Be 'Suspended' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -BeGreaterThan 0 # $webRequest | Should -BeNullOrEmpty diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 index 5b69fc903..65606460d 100644 --- a/tests/unit/Service.Tests.ps1 +++ b/tests/unit/Service.Tests.ps1 @@ -97,7 +97,7 @@ Describe 'Register-PodeService' { } # Act & Assert - Register-PodeService @params -ErrorAction SilentlyContinue| Should -BeFalse + Register-PodeService @params -ErrorAction SilentlyContinue | Should -BeFalse } } @@ -106,7 +106,6 @@ Describe 'Start-PodeService' { BeforeAll { # Mock the required commands Mock -CommandName Confirm-PodeAdminPrivilege - Mock -CommandName Get-Service Mock -CommandName Invoke-PodeWinElevatedCommand Mock -CommandName Test-PodeLinuxServiceIsRegistered Mock -CommandName Test-PodeLinuxServiceIsActive @@ -155,28 +154,50 @@ Describe 'Start-PodeService' { It 'Throws an error if the service is not registered' -Skip:(!$IsWindows) { - Start-PodeService -Name 'NonExistentService' -ErrorAction SilentlyContinue| Should -BeFalse + Start-PodeService -Name 'NonExistentService' -ErrorAction SilentlyContinue | Should -BeFalse } } Context 'On Linux platform' { It 'Starts a stopped service successfully' -Skip:(!$IsLinux) { + $script:status = $null + Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { + if ($null -eq $script:status ) { + $script:status = $false + } + else { + $script:status = $true + } + return $script:status + } + Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $true } - Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { $false } Mock -CommandName Start-PodeLinuxService -MockWith { $true } - Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { $true } # Act - Start-PodeService -Name 'LinuxService' | Should -Be $true + Start-PodeService -Name 'TestService' | Should -Be $true # Assert Assert-MockCalled -CommandName Start-PodeLinuxService -Exactly 1 } + It 'Starts a started service ' -Skip:(!$IsLinux) { + + Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { $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 0 + } + It 'Throws an error if the service is not registered' -Skip:(!$IsLinux) { Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $false } - { Start-PodeService -Name 'NonExistentService' } | Should -Throw + Start-PodeService -Name 'NonExistentService' | Should -BeFalse } } From 604056379145ae73c2d0b433df3be6734a47e297 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 23 Nov 2024 07:56:55 -0800 Subject: [PATCH 51/93] Update pode.build.ps1 --- pode.build.ps1 | 64 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index 788e4df64..c8470aee9 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -496,32 +496,75 @@ function Invoke-PodeBuildDotnetBuild { $AssemblyVersion = '' } - # Use dotnet publish for .NET Core and .NET 5+ + # Perform the build for the target runtime dotnet publish --configuration Release --self-contained --framework $target $AssemblyVersion --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 = $majorVersion -ge 8 + $isCompatible = $majorVersions -ge $requiredSdkVersion # Skip build if not compatible if ($isCompatible) { - Write-Host "SDK for target framework $target is compatible with the installed SDKs" + Write-Output "SDK for target framework '$target' is compatible with the '$AvailableSdkVersion' framework." } else { - Write-Host "SDK for target framework $target is not compatible with the installed SDKs. Skipping build." + 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" @@ -529,25 +572,29 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { else { $AssemblyVersion = '' } + foreach ($target in @('win-x64', 'win-arm64' , 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')) { $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)" } } - } <# @@ -927,8 +974,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 @@ -1126,9 +1171,6 @@ Add-BuildTask Build BuildDeps, { Remove-Item -Path ./src/Libs -Recurse -Force | Out-Null } - - - # Retrieve the SDK version being used # $dotnetVersion = dotnet --version @@ -1658,7 +1700,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 From 6ba641982c801452174b69d19c669d5be8a0bcd4 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 09:02:22 -0800 Subject: [PATCH 52/93] linux fixes --- src/Private/Helpers.ps1 | 2 +- src/Public/Service.ps1 | 8 +++----- tests/integration/Service.Tests.ps1 | 8 ++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index dc46ae62b..3fa85ca06 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3844,7 +3844,7 @@ function Test-PodeAdminPrivilege { # Check if the user has sudo privileges by checking sudo group membership $user = whoami $groups = (groups $user) - + Write-Output "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') } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index eee2d35c1..fc6116dfb 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -971,15 +971,13 @@ function Get-PodeService { $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 + $status = Get-Content -Path $stateFilePath -Raw + $status = $status.Substring(0, 1).ToUpper() + $status.Substring(1) } } 'reloading' { @@ -991,7 +989,7 @@ function Get-PodeService { $status = 'Suspended' } 'inactive' { - $status = 'Suspended' + $status = 'Stopped' } 'failed' { $status = 'Stopped' diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 27497c191..725b7e84f 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -17,7 +17,7 @@ Describe 'Service Lifecycle' { it 'start' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start - start-sleep 5 + Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -28,7 +28,7 @@ Describe 'Service Lifecycle' { it 'pause' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend - start-sleep 5 + Start-Sleep 2 # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Suspended' @@ -39,7 +39,7 @@ Describe 'Service Lifecycle' { it 'resume' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume - start-sleep 5 + Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -49,7 +49,7 @@ Describe 'Service Lifecycle' { } it 'stop' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - start-sleep 5 + Start-Sleep 2 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query From a57638659a884e93cb52052f3913b2480023a36d Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 09:25:10 -0800 Subject: [PATCH 53/93] support workflow debug --- .github/workflows/ci-powershell.yml | 6 +++++- .github/workflows/ci-pwsh7_5.yml | 6 +++++- .github/workflows/ci-pwsh_lts.yml | 6 +++++- .github/workflows/ci-pwsh_preview.yml | 6 +++++- tests/unit/Service.Tests.ps1 | 4 ++-- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-powershell.yml b/.github/workflows/ci-powershell.yml index adc4cd1f2..67fb6796a 100644 --- a/.github/workflows/ci-powershell.yml +++ b/.github/workflows/ci-powershell.yml @@ -52,7 +52,11 @@ jobs: shell: powershell run: | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-Build Test + if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + 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 1b0c51cce..c17ac0eda 100644 --- a/.github/workflows/ci-pwsh7_5.yml +++ b/.github/workflows/ci-pwsh7_5.yml @@ -72,7 +72,11 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Build Test + if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + 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..722d2c67d 100644 --- a/.github/workflows/ci-pwsh_lts.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -71,7 +71,11 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Build Test + if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + 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..5578e82fb 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -71,7 +71,11 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - Invoke-Build Test + if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + Invoke-Build Test -PesterVerbosity Diagnostic + } else { + Invoke-Build Test + } - name: Build Packages shell: pwsh diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 index 65606460d..838305a21 100644 --- a/tests/unit/Service.Tests.ps1 +++ b/tests/unit/Service.Tests.ps1 @@ -5,7 +5,7 @@ BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + #Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' } @@ -114,6 +114,7 @@ Describe 'Start-PodeService' { Mock -CommandName Test-PodeMacOsServiceIsActive Mock -CommandName Start-PodeMacOsService Mock -CommandName Write-PodeErrorLog + Mock -CommandName Write-Error } Context 'On Windows platform' { @@ -196,7 +197,6 @@ Describe 'Start-PodeService' { It 'Throws an error if the service is not registered' -Skip:(!$IsLinux) { Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $false } - Start-PodeService -Name 'NonExistentService' | Should -BeFalse } } From ae24dd94db8c2641998cfeb5361eaaaa47a74172 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 09:41:36 -0800 Subject: [PATCH 54/93] retry --- .github/workflows/ci-powershell.yml | 9 +++++++-- .github/workflows/ci-pwsh7_5.yml | 8 +++++++- .github/workflows/ci-pwsh_lts.yml | 8 +++++++- .github/workflows/ci-pwsh_preview.yml | 8 +++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-powershell.yml b/.github/workflows/ci-powershell.yml index 67fb6796a..c383ec5cf 100644 --- a/.github/workflows/ci-powershell.yml +++ b/.github/workflows/ci-powershell.yml @@ -51,8 +51,13 @@ jobs: - name: Run Pester Tests shell: powershell run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { + $debug = $false + } else { + $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + } + + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh7_5.yml b/.github/workflows/ci-pwsh7_5.yml index c17ac0eda..0d35145f7 100644 --- a/.github/workflows/ci-pwsh7_5.yml +++ b/.github/workflows/ci-pwsh7_5.yml @@ -72,7 +72,13 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { + $debug = $false + } else { + $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + } + + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh_lts.yml b/.github/workflows/ci-pwsh_lts.yml index 722d2c67d..b064514f4 100644 --- a/.github/workflows/ci-pwsh_lts.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -71,7 +71,13 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { + $debug = $false + } else { + $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + } + + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml index 5578e82fb..cef295b93 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -71,7 +71,13 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - if ($env:ACTIONS_STEP_DEBUG -eq 'true') { + if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { + $debug = $false + } else { + $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + } + + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test From bb1fbb294b3bdb72c170235371c299455179f982 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 10:03:58 -0800 Subject: [PATCH 55/93] try to catch $_ -like "*##[debug]*" --- .github/workflows/ci-powershell.yml | 13 ++++++++----- .github/workflows/ci-pwsh7_5.yml | 14 +++++++++----- .github/workflows/ci-pwsh_lts.yml | 14 +++++++++----- .github/workflows/ci-pwsh_preview.yml | 14 +++++++++----- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci-powershell.yml b/.github/workflows/ci-powershell.yml index c383ec5cf..8852e04a3 100644 --- a/.github/workflows/ci-powershell.yml +++ b/.github/workflows/ci-powershell.yml @@ -51,13 +51,16 @@ jobs: - name: Run Pester Tests shell: powershell run: | - if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { - $debug = $false - } else { - $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + $debugEnabled = $false + + # Check if logs are in debug mode by parsing an example debug output pattern + Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { + if ($_ -like "*##[debug]*") { + $debugEnabled = $true + } } - if ($debug) { + if ($debugEnabled) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh7_5.yml b/.github/workflows/ci-pwsh7_5.yml index 0d35145f7..2f63b3fa5 100644 --- a/.github/workflows/ci-pwsh7_5.yml +++ b/.github/workflows/ci-pwsh7_5.yml @@ -72,13 +72,17 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { - $debug = $false - } else { - $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + run: | + $debugEnabled = $false + + # Check if logs are in debug mode by parsing an example debug output pattern + Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { + if ($_ -like "*##[debug]*") { + $debugEnabled = $true + } } - if ($debug) { + if ($debugEnabled) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh_lts.yml b/.github/workflows/ci-pwsh_lts.yml index b064514f4..5953f1544 100644 --- a/.github/workflows/ci-pwsh_lts.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -71,13 +71,17 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { - $debug = $false - } else { - $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + run: | + $debugEnabled = $false + + # Check if logs are in debug mode by parsing an example debug output pattern + Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { + if ($_ -like "*##[debug]*") { + $debugEnabled = $true + } } - if ($debug) { + if ($debugEnabled) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml index cef295b93..391c483a9 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -71,13 +71,17 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - if ([string]::IsNullOrEmpty($env:ACTIONS_STEP_DEBUG)) { - $debug = $false - } else { - $debug = [bool]($env:ACTIONS_STEP_DEBUG -eq 'true') + run: | + $debugEnabled = $false + + # Check if logs are in debug mode by parsing an example debug output pattern + Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { + if ($_ -like "*##[debug]*") { + $debugEnabled = $true + } } - if ($debug) { + if ($debugEnabled) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test From f000a9eca1d1941794ff1d81678fd2f4d9a9527e Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 10:06:27 -0800 Subject: [PATCH 56/93] fix trhe workflow --- .github/workflows/ci-pwsh7_5.yml | 1 - .github/workflows/ci-pwsh_lts.yml | 1 - .github/workflows/ci-pwsh_preview.yml | 1 - 3 files changed, 3 deletions(-) diff --git a/.github/workflows/ci-pwsh7_5.yml b/.github/workflows/ci-pwsh7_5.yml index 2f63b3fa5..9f0a93b49 100644 --- a/.github/workflows/ci-pwsh7_5.yml +++ b/.github/workflows/ci-pwsh7_5.yml @@ -72,7 +72,6 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - run: | $debugEnabled = $false # Check if logs are in debug mode by parsing an example debug output pattern diff --git a/.github/workflows/ci-pwsh_lts.yml b/.github/workflows/ci-pwsh_lts.yml index 5953f1544..2d6e83e68 100644 --- a/.github/workflows/ci-pwsh_lts.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -71,7 +71,6 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - run: | $debugEnabled = $false # Check if logs are in debug mode by parsing an example debug output pattern diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml index 391c483a9..df33333d7 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -71,7 +71,6 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - run: | $debugEnabled = $false # Check if logs are in debug mode by parsing an example debug output pattern From 1b188e67680f356fb85e0de09477e401deb5fa36 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 10:11:38 -0800 Subject: [PATCH 57/93] again --- .github/workflows/ci-powershell.yml | 14 ++++++-------- .github/workflows/ci-pwsh7_5.yml | 14 ++++++-------- .github/workflows/ci-pwsh_lts.yml | 14 ++++++-------- .github/workflows/ci-pwsh_preview.yml | 14 ++++++-------- 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci-powershell.yml b/.github/workflows/ci-powershell.yml index 8852e04a3..900d1806b 100644 --- a/.github/workflows/ci-powershell.yml +++ b/.github/workflows/ci-powershell.yml @@ -51,16 +51,14 @@ jobs: - name: Run Pester Tests shell: powershell run: | - $debugEnabled = $false - - # Check if logs are in debug mode by parsing an example debug output pattern - Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { - if ($_ -like "*##[debug]*") { - $debugEnabled = $true - } + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false } - if ($debugEnabled) { + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh7_5.yml b/.github/workflows/ci-pwsh7_5.yml index 9f0a93b49..fe19e54ff 100644 --- a/.github/workflows/ci-pwsh7_5.yml +++ b/.github/workflows/ci-pwsh7_5.yml @@ -72,16 +72,14 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - $debugEnabled = $false - - # Check if logs are in debug mode by parsing an example debug output pattern - Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { - if ($_ -like "*##[debug]*") { - $debugEnabled = $true - } + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false } - if ($debugEnabled) { + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh_lts.yml b/.github/workflows/ci-pwsh_lts.yml index 2d6e83e68..de2e79856 100644 --- a/.github/workflows/ci-pwsh_lts.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -71,16 +71,14 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - $debugEnabled = $false - - # Check if logs are in debug mode by parsing an example debug output pattern - Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { - if ($_ -like "*##[debug]*") { - $debugEnabled = $true - } + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false } - if ($debugEnabled) { + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml index df33333d7..60c2e9c0d 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -71,16 +71,14 @@ jobs: - name: Run Pester Tests shell: pwsh run: | - $debugEnabled = $false - - # Check if logs are in debug mode by parsing an example debug output pattern - Get-Content -Path $env:GITHUB_ACTION_LOG -ErrorAction SilentlyContinue | ForEach-Object { - if ($_ -like "*##[debug]*") { - $debugEnabled = $true - } + # Check if the runner is in debug mode + if ($env:RUNNER_DEBUG -eq '1') { + $debug = $true + } else { + $debug = $false } - if ($debugEnabled) { + if ($debug) { Invoke-Build Test -PesterVerbosity Diagnostic } else { Invoke-Build Test From 55df3ab5fe124fb1657588faf5292039c2a894d2 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 10:53:44 -0800 Subject: [PATCH 58/93] adding delays --- src/Private/Helpers.ps1 | 2 +- tests/integration/Service.Tests.ps1 | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 3fa85ca06..6276e5a5c 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3844,7 +3844,7 @@ function Test-PodeAdminPrivilege { # Check if the user has sudo privileges by checking sudo group membership $user = whoami $groups = (groups $user) - Write-Output "User:$user Groups: $( $groups -join ',')" + 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') } diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 725b7e84f..008d3c120 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -17,7 +17,7 @@ Describe 'Service Lifecycle' { it 'start' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start - Start-Sleep 2 + Start-Sleep 10 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -28,7 +28,7 @@ Describe 'Service Lifecycle' { it 'pause' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend - Start-Sleep 2 + Start-Sleep 10 # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Suspended' @@ -39,7 +39,7 @@ Describe 'Service Lifecycle' { it 'resume' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume - Start-Sleep 2 + Start-Sleep 10 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -49,7 +49,7 @@ Describe 'Service Lifecycle' { } it 'stop' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - Start-Sleep 2 + Start-Sleep 10 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query @@ -63,6 +63,7 @@ Describe 'Service Lifecycle' { } it 'unregister' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Unregister + Start-Sleep 2 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status | Should -BeNullOrEmpty } From 7d208c636d97837adaf6ff12e33bcb0b0238aae3 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 11:11:41 -0800 Subject: [PATCH 59/93] build improvements --- pode.build.ps1 | 53 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index c8470aee9..f16c8091a 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -1318,11 +1318,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 (Test-PodeIsUnix) { + $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' + + Write-Output 'Pester Execution Context (Linux):' + Write-Output " - User: $user" + Write-Output " - Groups: $( $groups -join ', ')" + Write-Output " - Sudo: $($isSudoUser -eq $true)" + } + + if (Test-PodeIsMacOS) { + $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" From d6946f3de2e5b6b9ace50ca481d3cd428ffe2d27 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 11:16:41 -0800 Subject: [PATCH 60/93] replace Test-PodeBuildIsWindows with $iswindows --- pode.build.ps1 | 47 +++++++++++------------------------------------ 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index f16c8091a..81277d316 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -127,32 +127,7 @@ $Versions = @{ # Helper Functions - -<# -.SYNOPSIS - Checks if the current environment is running on Windows. - -.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. - -.OUTPUTS - [bool] - Returns `$true` if the current environment is Windows, otherwise `$false`. - -.EXAMPLE - if (Test-PodeBuildIsWindows) { - Write-Host "This script is running on Windows." - } - -.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. -#> -function Test-PodeBuildIsWindows { - $v = $PSVersionTable - return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop')) -} + <# .SYNOPSIS @@ -251,12 +226,12 @@ function Get-PodeBuildService { .NOTES - This function supports both Windows and Unix-based platforms. - - Requires `Test-PodeBuildIsWindows` to detect the OS type. + - Requires `$IsWindows` to detect the OS type. #> function Test-PodeBuildCommand($cmd) { $path = $null - if (Test-PodeBuildIsWindows) { + if ($IsWindows) { $path = (Get-Command $cmd -ErrorAction Ignore) } else { @@ -322,7 +297,7 @@ function Get-PodeBuildBranch { function Invoke-PodeBuildInstall($name, $version) { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - if (Test-PodeBuildIsWindows) { + if ($IsWindows) { if (Test-PodeBuildCommand 'choco') { choco install $name --version $version -y --no-progress } @@ -1049,7 +1024,7 @@ Add-BuildTask PrintChecksum { #> # Synopsis: Installs Chocolatey -Add-BuildTask ChocoDeps -If (Test-PodeBuildIsWindows) { +Add-BuildTask ChocoDeps -If ($IsWindows) { if (!(Test-PodeBuildCommand 'choco')) { Set-ExecutionPolicy Bypass -Scope Process -Force Invoke-Expression ([System.Net.WebClient]::new().DownloadString('https://chocolatey.org/install.ps1')) @@ -1059,7 +1034,7 @@ Add-BuildTask ChocoDeps -If (Test-PodeBuildIsWindows) { # Synopsis: Install dependencies for compiling/building Add-BuildTask BuildDeps { # install dotnet - if (Test-PodeBuildIsWindows) { + if ($IsWindows) { $dotnet = 'dotnet' } elseif (Test-PodeBuildCommand 'brew') { @@ -1235,7 +1210,7 @@ Add-BuildTask Compress PackageFolder, StampVersion, DeliverableFolder, { }, PrintChecksum # Synopsis: Creates a Chocolately package of the Module -Add-BuildTask ChocoPack -If (Test-PodeBuildIsWindows) ChocoDeps, PackageFolder, StampVersion, DeliverableFolder, { +Add-BuildTask ChocoPack -If ($IsWindows) ChocoDeps, PackageFolder, StampVersion, DeliverableFolder, { exec { choco pack ./packers/choco/pode.nuspec } Move-Item -Path "pode.$Version.nupkg" -Destination './deliverable' } @@ -1243,7 +1218,7 @@ Add-BuildTask ChocoPack -If (Test-PodeBuildIsWindows) ChocoDeps, PackageFolder, # Synopsis: Create docker tags Add-BuildTask DockerPack PackageFolder, StampVersion, { # check if github and windows, and output warning - if ((Test-PodeBuildIsGitHub) -and (Test-PodeBuildIsWindows)) { + if ((Test-PodeBuildIsGitHub) -and ($IsWindows)) { Write-Warning 'Docker images are not built on GitHub Windows runners, and Docker is in Windows container only mode. Exiting task.' return } @@ -1320,7 +1295,7 @@ Add-BuildTask TestNoBuild TestDeps, { } Write-Output '' # for windows, output current netsh excluded ports - if (Test-PodeBuildIsWindows) { + if ($IsWindows) { netsh int ipv4 show excludedportrange protocol=tcp | Out-Default # Retrieve the current Windows identity and token @@ -1349,7 +1324,7 @@ Add-BuildTask TestNoBuild TestDeps, { } - if (Test-PodeIsUnix) { + if ($IsLinux) { $user = whoami $groupsRaw = (groups $user | Out-String).Trim() $groups = $groupsRaw -split '\s+' | Where-Object { $_ -ne ':' } | Sort-Object -Unique @@ -1363,7 +1338,7 @@ Add-BuildTask TestNoBuild TestDeps, { Write-Output " - Sudo: $($isSudoUser -eq $true)" } - if (Test-PodeIsMacOS) { + if ($IsMacOS) { $user = whoami $groups = (id -Gn $user).Split(' ') # Use `id -Gn` for consistent group names on macOS $formattedGroups = $groups -join ', ' From ad7a7e87d694447d261f5c96ab1798378f2ddd51 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 11:20:40 -0800 Subject: [PATCH 61/93] add group adm as sudo users --- pode.build.ps1 | 4 ++-- src/Private/Helpers.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index 81277d316..f51e23bf2 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -127,7 +127,7 @@ $Versions = @{ # Helper Functions - + <# .SYNOPSIS @@ -1330,7 +1330,7 @@ Add-BuildTask TestNoBuild TestDeps, { $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' + $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" diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 6276e5a5c..1c801a591 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3846,7 +3846,7 @@ function Test-PodeAdminPrivilege { $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') + return ($groups -match '\bwheel\b' -or $groups -match '\badmin\b' -or $groups -match '\bsudo\b' -or $groups -match '\badm\b') } return $false } From d18b1f6065a7b8ec349a6e00a017414435195dac Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 23 Nov 2024 14:49:54 -0800 Subject: [PATCH 62/93] Fix the macos part --- docs/Hosting/RunAsService.md | 3 ++ examples/HelloService/HelloService.ps1 | 2 +- src/PodeMonitor/PodeMonitor.cs | 28 ++++++++++---- src/Private/Service.ps1 | 46 +++++++++++++++++----- src/Public/Service.ps1 | 29 +++++++++++--- tests/integration/Service.Tests.ps1 | 53 +++++++++++++++++++++----- tests/unit/Service.Tests.ps1 | 22 +++++++---- 7 files changed, 144 insertions(+), 39 deletions(-) diff --git a/docs/Hosting/RunAsService.md b/docs/Hosting/RunAsService.md index 7a33db3d8..734a7fbb8 100644 --- a/docs/Hosting/RunAsService.md +++ b/docs/Hosting/RunAsService.md @@ -54,6 +54,9 @@ The `Register-PodeService` function offers several parameters to customize your - **`-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). + - **`-Start`** *(switch)*: Starts the service immediately after registration. diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index a948dd86b..69ac3570a 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -141,7 +141,7 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service' -ParameterString "-Port $Port" -Password $Password + Register-PodeService -Name 'Hello Service' -ParameterString "-Port $Port" -Password $Password -Agent exit } if ( $Unregister.IsPresent) { diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 1409c27e8..4176746f4 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -77,21 +77,33 @@ public PodeMonitor(PodeMonitorWorkerOptions options) // Define the state file path only for Linux/macOS if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { - string stateDirectory = OperatingSystem.IsLinux() ? "/run/podemonitor" : - OperatingSystem.IsMacOS() ? "/private/var/run/podemonitor" : - throw new PlatformNotSupportedException("The current platform is not supported for setting the state directory."); - if (!Directory.Exists(stateDirectory)) + 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 { - Directory.CreateDirectory(stateDirectory); + if (!Directory.Exists(stateDirectory)) + { + Directory.CreateDirectory(stateDirectory); + } } + catch (Exception ex) + { + PodeMonitorLogger.Log(LogLevel.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(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName} and state file: {_stateFilePath}"); } - } /// @@ -180,8 +192,10 @@ public void StopPowerShellProcess() PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); - if (_powerShellProcess != null || !_powerShellProcess.HasExited || Process.GetProcessById(_powerShellProcess.Id) != null) + if (_powerShellProcess != null && !_powerShellProcess.HasExited ) { + PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Pode process has exited:{_powerShellProcess.HasExited} Id:{_powerShellProcess.Id}"); + PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully. Killing process."); _powerShellProcess.Kill(); } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 20b620831..21dbbeba7 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -57,7 +57,7 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { - $serviceState='running' + $serviceState = 'running' while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { Write-PodeHost -Message "Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force @@ -201,6 +201,9 @@ function Start-PodeServiceHearthbeat { .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. @@ -235,7 +238,10 @@ function Register-PodeMacService { $OsArchitecture, [string] - $LogPath + $LogPath, + + [switch] + $Agent ) $nameService = "pode.$Name.service".Replace(' ', '_') @@ -248,7 +254,12 @@ function Register-PodeMacService { # Determine whether the service should run at load $runAtLoad = if ($Autostart.IsPresent) { '' } else { '' } - + if ($Agent) { + $plistPath = "$($HOME)/Library/LaunchAgents/$($nameService).plist" + } + else { + $plistPath = "/Library/LaunchDaemons/$($nameService).plist" + } # Create the plist content @" @@ -288,16 +299,20 @@ function Register-PodeMacService { --> -"@ | Set-Content -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -Encoding UTF8 +"@ | Set-Content -Path $plistPath -Encoding UTF8 Write-Verbose -Message "Service '$nameService' WorkingDirectory : $($BinPath)." - chmod +r "$($HOME)/Library/LaunchAgents/$($nameService).plist" + chmod +r $plistPath try { # Load the plist with launchctl - launchctl load "$($HOME)/Library/LaunchAgents/$($nameService).plist" - + if ($Agent) { + launchctl load $plistPath + } + else { + sudo launchctl load $plistPat + } # Verify the service is now registered if (! (Test-PodeMacOsServiceIsRegistered $nameService)) { # Service registration failed. @@ -1011,7 +1026,13 @@ function Disable-PodeMacOsService { [string] $Name ) - $systemctlDisable = launchctl unload "$HOME/Library/LaunchAgents/$Name.plist" 2>&1 + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($Name).plist" -PathType Leaf) + if ($sudo) { + $systemctlDisable = sudo launchctl unload "/Library/LaunchDaemons/$Name.plist" 2>&1 + } + else { + $systemctlDisable = launchctl unload "$HOME/Library/LaunchAgents/$Name.plist" 2>&1 + } $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($systemctlDisable -join '`n') return $success @@ -1073,7 +1094,13 @@ function Start-PodeMacOsService { [string] $Name ) - $serviceStartInfo = launchctl start $Name 2>&1 + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + if ($sudo) { + $serviceStartInfo = sudo launchctl start $Name 2>&1 + } + else { + $serviceStartInfo = launchctl start $Name 2>&1 + } $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($serviceStartInfo -join "`n") return $success @@ -1101,6 +1128,7 @@ function Send-PodeServiceSignal { 'SIGHUP' = 1 'SIGTERM' = 15 } + $level = $signalMap[$Signal] # Check if the service is registered if ((Test-PodeServiceIsRegistered -Name $nameService)) { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index fc6116dfb..8f37b1ccc 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -40,6 +40,9 @@ .PARAMETER LinuxUser Specifies the username under which the service will run by default is the current user (Linux Only). +.PARAMETER Agent + A switch to create an Agent instead of a Daemon in MacOS (MacOS Only). + .PARAMETER Start A switch to start the service immediately after registration. @@ -109,6 +112,9 @@ function Register-PodeService { [switch] $Start, + [switch] + $Agent, + [securestring] $Password, @@ -240,6 +246,7 @@ function Register-PodeService { User = $User OsArchitecture = "osx-$osArchitecture" LogPath = $LogPath + Agent = $Agent } $operation = Register-PodeMacService @param @@ -846,7 +853,13 @@ function Unregister-PodeService { } if ((Disable-PodeMacOsService -Name $nameService)) { - $plistFilePath = "$HOME/Library/LaunchAgents/$nameService.plist" + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + if ($sudo) { + $plistFilePath = "/Library/LaunchDaemons/$nameService.plist" + } + else { + $plistFilePath = "$HOME/Library/LaunchAgents/$nameService.plist" + } #Check if the plist file exists if (Test-Path -Path $plistFilePath) { # Read the content of the plist file @@ -1031,12 +1044,18 @@ function Get-PodeService { $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) - $stateFilePath = "/private/var/run/podemonitor/$servicePid.state" - if (Test-Path -Path $stateFilePath) { - $status = Get-Content -Path $stateFilePath - } + # Check if the service has a PID entry if ($servicePid -ne 0) { + if ($sudo) { + $stateFilePath = "/Library/LaunchDaemons/PodeMonitor/$servicePid.state" + } + else { + $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" + } + if (Test-Path -Path $stateFilePath) { + $status = Get-Content -Path $stateFilePath + } return @{ Name = $Name Status = $status diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 008d3c120..6f6ccd44e 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -8,16 +8,25 @@ Describe 'Service Lifecycle' { it 'register' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register + Start-Sleep 8 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query - $status.Status | Should -Be 'Stopped' + 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' - $status.Pid | Should -Be 0 + } - it 'start' { + it 'start' -Skip:(!$IsMacOS) { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start - Start-Sleep 10 + Start-Sleep 8 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -28,18 +37,18 @@ Describe 'Service Lifecycle' { it 'pause' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend - Start-Sleep 10 - # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + Start-Sleep 8 + # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Suspended' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -BeGreaterThan 0 - # $webRequest | Should -BeNullOrEmpty + # $webRequest | Should -BeNullOrEmpty } it 'resume' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume - Start-Sleep 10 + Start-Sleep 8 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -49,7 +58,7 @@ Describe 'Service Lifecycle' { } it 'stop' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - Start-Sleep 10 + Start-Sleep 8 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query @@ -57,10 +66,34 @@ Describe 'Service Lifecycle' { $status.Name | Should -Be 'Hello Service' $status.Pid | Should -Be 0 - {Invoke-WebRequest -uri http://localhost:8080 }| Should -Throw + { Invoke-WebRequest -uri http://localhost:8080 } | Should -Throw + } + + it 're-start' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start + Start-Sleep 8 + $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status.Status | Should -Be 'Running' + $status.Name | Should -Be 'Hello Service' + $status.Pid | Should -BeGreaterThan 0 + $webRequest.Content | Should -Be 'Hello, Service!' + } + + + it 're-stop' { + . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop + Start-Sleep 8 + $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $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 'unregister' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Unregister Start-Sleep 2 diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 index 838305a21..aa066bfc0 100644 --- a/tests/unit/Service.Tests.ps1 +++ b/tests/unit/Service.Tests.ps1 @@ -4,8 +4,7 @@ param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' - Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - #Import-LocalizedData -BindingVariable PodeLocale -BaseDirectory (Join-Path -Path $src -ChildPath 'Locales') -FileName 'Pode' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } } @@ -195,7 +194,7 @@ Describe 'Start-PodeService' { Assert-MockCalled -CommandName Start-PodeLinuxService -Exactly 0 } - It 'Throws an error if the service is not registered' -Skip:(!$IsLinux) { + It 'Return false if the service is not registered' -Skip:(!$IsLinux) { Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $false } Start-PodeService -Name 'NonExistentService' | Should -BeFalse } @@ -204,9 +203,18 @@ Describe 'Start-PodeService' { Context 'On macOS platform' { It 'Starts a stopped service successfully' -Skip:(!$IsMacOS) { Mock -CommandName Test-PodeMacOsServiceIsRegistered -MockWith { $true } - Mock -CommandName Test-PodeMacOsServiceIsActive -MockWith { $false } Mock -CommandName Start-PodeMacOsService -MockWith { $true } - Mock -CommandName Test-PodeMacOsServiceIsActive -MockWith { $true } + + $script:status = $null + Mock -CommandName Test-PodeMacOsServiceIsActive -MockWith { + if ($null -eq $script:status ) { + $script:status = $false + } + else { + $script:status = $true + } + return $script:status + } # Act Start-PodeService -Name 'MacService' | Should -Be $true @@ -215,10 +223,10 @@ Describe 'Start-PodeService' { Assert-MockCalled -CommandName Start-PodeMacOsService -Exactly 1 } - It 'Throws an error if the service is not registered' -Skip:(!$IsMacOS) { + It 'Return false if the service is not registered' -Skip:(!$IsMacOS) { Mock -CommandName Test-PodeMacOsServiceIsRegistered -MockWith { $false } - { Start-PodeService -Name 'NonExistentService' } | Should -Throw + Start-PodeService -Name 'NonExistentService' | Should -BeFalse } } } From 304457d066f45c825d26b6982f8e7e360a4b168c Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 23 Nov 2024 15:50:24 -0800 Subject: [PATCH 63/93] modified: tests/integration/Service.Tests.ps1 --- tests/integration/Service.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 6f6ccd44e..6c119b970 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -24,7 +24,7 @@ Describe 'Service Lifecycle' { } - it 'start' -Skip:(!$IsMacOS) { + it 'start' -Skip:( $IsMacOS) { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start Start-Sleep 8 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue From 6af3213c8cdc8c86268b26bf2ec75ae4954bfff8 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 23 Nov 2024 17:37:52 -0800 Subject: [PATCH 64/93] tests fix --- pode.build.ps1 | 138 +++++++++++++++++----------- src/Private/Helpers.ps1 | 2 +- src/Private/Service.ps1 | 27 +++--- src/Public/Service.ps1 | 70 ++++++++------ tests/integration/Service.Tests.ps1 | 16 ++-- tests/unit/Service.Tests.ps1 | 4 +- 6 files changed, 150 insertions(+), 107 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index f51e23bf2..eca54c59f 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -128,6 +128,54 @@ $Versions = @{ # Helper Functions +<# +.SYNOPSIS + Installs a specified package using the appropriate package manager for the OS. + +.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. + +.OUTPUTS + None. + +.EXAMPLE + Invoke-PodeBuildInstall -Name 'git' -Version '2.30.0' + # Installs version 2.30.0 of Git on Windows if Chocolatey is available. + +.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. +#> +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 + } + } +} <# .SYNOPSIS @@ -226,12 +274,12 @@ function Get-PodeBuildService { .NOTES - This function supports both Windows and Unix-based platforms. - - Requires `$IsWindows` to detect the OS type. + - Requires `Test-PodeBuildIsWindows` to detect the OS type. #> function Test-PodeBuildCommand($cmd) { $path = $null - if ($IsWindows) { + if (Test-PodeBuildIsWindows) { $path = (Get-Command $cmd -ErrorAction Ignore) } else { @@ -243,76 +291,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 ($IsWindows) { - 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\/', '') } <# @@ -620,7 +646,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. @@ -1024,7 +1050,7 @@ Add-BuildTask PrintChecksum { #> # Synopsis: Installs Chocolatey -Add-BuildTask ChocoDeps -If ($IsWindows) { +Add-BuildTask ChocoDeps -If (Test-PodeBuildIsWindows) { if (!(Test-PodeBuildCommand 'choco')) { Set-ExecutionPolicy Bypass -Scope Process -Force Invoke-Expression ([System.Net.WebClient]::new().DownloadString('https://chocolatey.org/install.ps1')) @@ -1034,7 +1060,7 @@ Add-BuildTask ChocoDeps -If ($IsWindows) { # Synopsis: Install dependencies for compiling/building Add-BuildTask BuildDeps { # install dotnet - if ($IsWindows) { + if (Test-PodeBuildIsWindows) { $dotnet = 'dotnet' } elseif (Test-PodeBuildCommand 'brew') { @@ -1210,7 +1236,7 @@ Add-BuildTask Compress PackageFolder, StampVersion, DeliverableFolder, { }, PrintChecksum # Synopsis: Creates a Chocolately package of the Module -Add-BuildTask ChocoPack -If ($IsWindows) ChocoDeps, PackageFolder, StampVersion, DeliverableFolder, { +Add-BuildTask ChocoPack -If (Test-PodeBuildIsWindows) ChocoDeps, PackageFolder, StampVersion, DeliverableFolder, { exec { choco pack ./packers/choco/pode.nuspec } Move-Item -Path "pode.$Version.nupkg" -Destination './deliverable' } @@ -1218,7 +1244,7 @@ Add-BuildTask ChocoPack -If ($IsWindows) ChocoDeps, PackageFolder, StampVersion, # Synopsis: Create docker tags Add-BuildTask DockerPack PackageFolder, StampVersion, { # check if github and windows, and output warning - if ((Test-PodeBuildIsGitHub) -and ($IsWindows)) { + if ((Test-PodeBuildIsGitHub) -and (Test-PodeBuildIsWindows)) { Write-Warning 'Docker images are not built on GitHub Windows runners, and Docker is in Windows container only mode. Exiting task.' return } @@ -1295,7 +1321,7 @@ Add-BuildTask TestNoBuild TestDeps, { } Write-Output '' # for windows, output current netsh excluded ports - if ($IsWindows) { + if (Test-PodeBuildIsWindows) { netsh int ipv4 show excludedportrange protocol=tcp | Out-Default # Retrieve the current Windows identity and token diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 1c801a591..8b33314ff 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3802,7 +3802,7 @@ function Test-PodeAdminPrivilege { ) try { # Check if the operating system is Windows - if ($IsWindows) { + if (Test-PodeIsWindows) { # Retrieve the current Windows identity and token $identity = [Security.Principal.WindowsIdentity]::GetCurrent() diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 21dbbeba7..7680da809 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -638,7 +638,7 @@ function Test-PodeUserServiceCreationPrivilege { function Confirm-PodeAdminPrivilege { # Check for administrative privileges if (! (Test-PodeAdminPrivilege -Elevate)) { - if ($IsWindows -and (Test-PodeUserServiceCreationPrivilege)) { + 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 } @@ -887,16 +887,16 @@ function Test-PodeServiceIsRegistered { [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 } - if ($IsWindows) { - $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" - return $null -eq $service - } } <# @@ -925,13 +925,7 @@ function Test-PodeServiceIsActive { [string] $Name ) - if ($IsLinux) { - return Test-PodeLinuxServiceIsActive -Name $Name - } - if ($IsMacOS) { - return Test-PodeMacOsServiceIsActive -Name $Name - } - if ($IsWindows) { + if (Test-PodeIsWindows) { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is already running @@ -939,6 +933,13 @@ function Test-PodeServiceIsActive { } return $false } + if ($IsLinux) { + return Test-PodeLinuxServiceIsActive -Name $Name + } + if ($IsMacOS) { + return Test-PodeMacOsServiceIsActive -Name $Name + } + } @@ -1128,7 +1129,7 @@ function Send-PodeServiceSignal { 'SIGHUP' = 1 'SIGTERM' = 15 } - + $level = $signalMap[$Signal] # Check if the service is registered if ((Test-PodeServiceIsRegistered -Name $nameService)) { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 8f37b1ccc..be973bc68 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -160,7 +160,7 @@ function Register-PodeService { if (! (Test-Path -Path $SettingsPath -PathType Container)) { $null = New-Item -Path $settingsPath -ItemType Directory } - if ($IsWindows) { + if (Test-PodeIsWindows) { if ([string]::IsNullOrEmpty($WindowsUser)) { if ( ($null -ne $Password)) { $UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name @@ -210,7 +210,7 @@ function Register-PodeService { # 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 ($IsWindows) { + if (Test-PodeIsWindows) { $param = @{ Name = $Name Description = $Description @@ -304,22 +304,25 @@ function Start-PodeService { try { - if ($IsWindows) { + if (Test-PodeIsWindows) { # Get the Windows service $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is already running if ($service.Status -eq 'Stopped') { - $null = Invoke-PodeWinElevatedCommand -Command 'Start-Service' -Arguments "-Name '$Name'" + if( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "start '$Name'"){ + return $true + } - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + <# $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Running') { Write-Verbose -Message "Service '$Name' started successfully." } else { + #> throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $Name) - } + # } } else { # Log service is already running @@ -341,11 +344,13 @@ function Start-PodeService { if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { # Start the service if ((Start-PodeLinuxService -Name $nameService)) { + return $true # Check if the service is active - if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { + <# if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$nameService' started successfully." return $true - } + }#> + } # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $nameService) @@ -370,10 +375,11 @@ function Start-PodeService { # Start the service if ((Start-PodeMacOsService -Name $nameService)) { # Check if the service is active - if ((Test-PodeMacOsServiceIsActive -Name $nameService)) { + return $true + <# if ((Test-PodeMacOsServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$nameService' started successfully." return $true - } + }#> } # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $nameService) @@ -432,14 +438,17 @@ function Stop-PodeService { # Exits the script if the current user lacks the required privileges. Confirm-PodeAdminPrivilege - if ($IsWindows) { + if (Test-PodeIsWindows) { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running if ($service.Status -eq 'Running' -or $service.Status -eq 'Paused') { - $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + if( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "stop '$Name'") + { + return $true + } + <# $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Stopped') { Write-Verbose -Message "Service '$Name' stopped successfully." } @@ -447,6 +456,7 @@ function Stop-PodeService { # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } + #> } else { Write-Verbose -Message "Service '$Name' is not running." @@ -465,15 +475,18 @@ function Stop-PodeService { if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { #Stop the service if (( Stop-PodeLinuxService -Name $Name)) { + return $true # Check if the service is active - if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { + <# if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$Name' stopped successfully." return $true - } + }#> + } # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'sudo launchctl stop', $Name) + } else { Write-Verbose -Message "Service '$Name' is not running." @@ -492,13 +505,15 @@ function Stop-PodeService { # Check if the service is active if ((Test-PodeMacOsServiceIsActive $nameService)) { if ((Stop-PodeMacOsService $Name)) { - for ($i = 0; $i -lt 30; $i++) { + return $true + <# for ($i = 0; $i -lt 30; $i++) { if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { Write-Verbose -Message "Service '$Name' stopped successfully." return $true } Start-Sleep 1 } + }#> } # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) @@ -551,20 +566,21 @@ function Suspend-PodeService { # Exits the script if the current user lacks the required privileges. Confirm-PodeAdminPrivilege - if ($IsWindows) { + if (Test-PodeIsWindows) { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running if ($service.Status -eq 'Running') { - $null = Invoke-PodeWinElevatedCommand -Command 'Suspend-Service' -Arguments "-Name '$Name'" - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue + $null = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "pause '$Name'" + + <# $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Paused') { Write-Verbose -Message "Service '$Name' suspended successfully." } else { # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'Suspend-Service', $Name) - } + }#> } else { Write-Verbose -Message "Service '$Name' is not running." @@ -616,13 +632,14 @@ function Resume-PodeService { # Ensure the script is running with the necessary administrative/root privileges. # Exits the script if the current user lacks the required privileges. Confirm-PodeAdminPrivilege - if ($IsWindows) { + if (Test-PodeIsWindows) { $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { # Check if the service is running if ($service.Status -eq 'Paused') { - $null = Invoke-PodeWinElevatedCommand -Command 'Resume-Service' -Arguments "-Name '$Name'" + $null = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "continue '$Name'" + <# $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service.Status -eq 'Running') { Write-Verbose -Message "Service '$Name' Resumed successfully." @@ -631,6 +648,7 @@ function Resume-PodeService { # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'Resume-Service', $Name) } + #> } else { Write-Verbose -Message "Service '$Name' is not suspended." @@ -699,7 +717,7 @@ function Unregister-PodeService { # Exits the script if the current user lacks the required privileges. Confirm-PodeAdminPrivilege - if ($IsWindows) { + if (Test-PodeIsWindows) { # Check if the service exists $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if (-not $service) { @@ -729,7 +747,7 @@ function Unregister-PodeService { } # Remove the service - $null = Invoke-PodeWinElevatedCommand -Command 'Remove-Service' -Arguments "-Name '$Name'" + $null = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "delete '$Name'" $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($null -ne $service) { Write-Verbose -Message "Service '$Name' unregistered failed." @@ -948,7 +966,7 @@ function Get-PodeService { # Exits the script if the current user lacks the required privileges. Confirm-PodeAdminPrivilege - if ($IsWindows) { + if (Test-PodeIsWindows) { # Check if the service exists on Windows $service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'" @@ -1131,7 +1149,7 @@ function Restart-PodeService { Write-Verbose -Message "Attempting to restart service '$Name' on platform $([System.Environment]::OSVersion.Platform)..." - if ($IsWindows) { + if (Test-PodeIsWindows) { # Handle Windows-specific restart logic $service = Get-Service -Name $Name -ErrorAction SilentlyContinue if ($service) { diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 6c119b970..fc07eb65e 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -8,7 +8,7 @@ Describe 'Service Lifecycle' { it 'register' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register - Start-Sleep 8 + Start-Sleep 10 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query if ($IsMacOS) { $status.Status | Should -Be 'Running' @@ -26,7 +26,7 @@ Describe 'Service Lifecycle' { it 'start' -Skip:( $IsMacOS) { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start - Start-Sleep 8 + Start-Sleep 10 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -37,7 +37,7 @@ Describe 'Service Lifecycle' { it 'pause' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend - Start-Sleep 8 + Start-Sleep 10 # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Suspended' @@ -48,7 +48,7 @@ Describe 'Service Lifecycle' { it 'resume' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume - Start-Sleep 8 + Start-Sleep 10 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -58,9 +58,7 @@ Describe 'Service Lifecycle' { } it 'stop' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - Start-Sleep 8 - - + Start-Sleep 10 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Stopped' $status.Name | Should -Be 'Hello Service' @@ -71,7 +69,7 @@ Describe 'Service Lifecycle' { it 're-start' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start - Start-Sleep 8 + Start-Sleep 10 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -83,7 +81,7 @@ Describe 'Service Lifecycle' { it 're-stop' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - Start-Sleep 8 + Start-Sleep 10 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 index aa066bfc0..ad61e0cf3 100644 --- a/tests/unit/Service.Tests.ps1 +++ b/tests/unit/Service.Tests.ps1 @@ -4,7 +4,7 @@ param() BeforeAll { $path = $PSCommandPath $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' - Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } } @@ -129,7 +129,7 @@ Describe 'Start-PodeService' { } [pscustomobject]@{ Name = 'TestService'; Status = $status } } - Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { $null } + Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { return $true } # Act Start-PodeService -Name 'TestService' | Should -Be $true From 12c33a4eeb633be0b8fc9544d6dbcf529011252c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Nov 2024 14:31:28 -0800 Subject: [PATCH 65/93] improvements to the service commands --- src/PodeMonitor/PodeMonitorWorker.cs | 2 +- src/Private/Service.ps1 | 413 +++++++++++++- src/Public/Service.ps1 | 819 +++++++++++++-------------- 3 files changed, 790 insertions(+), 444 deletions(-) diff --git a/src/PodeMonitor/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs index a25a0ab3f..397128956 100644 --- a/src/PodeMonitor/PodeMonitorWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -124,7 +124,7 @@ public void Shutdown() /// public void Restart() { - if ((!_terminating) && _pwshMonitor.State == ServiceState.Running) + if ((!_terminating) && _pwshMonitor.State == ServiceState.Running || _pwshMonitor.State == ServiceState.Suspended) { PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); try diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 7680da809..9a73bfa80 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -244,7 +244,7 @@ function Register-PodeMacService { $Agent ) - $nameService = "pode.$Name.service".Replace(' ', '_') + $nameService = Get-PodeRealServiceName # Check if the service is already registered if ((Test-PodeMacOsServiceIsRegistered $nameService)) { @@ -410,7 +410,7 @@ function Register-PodeLinuxService { [string] $OsArchitecture ) - $nameService = "$Name.service".Replace(' ', '_') + $nameService = Get-PodeRealServiceName $null = systemctl status $nameService 2>&1 # Check if the service is already registered @@ -673,7 +673,9 @@ function Test-PodeLinuxServiceIsRegistered { [string] $Name ) - $systemctlStatus = systemctl status $Name 2>&1 + + $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 @@ -703,7 +705,8 @@ function Test-PodeLinuxServiceIsActive { [string] $Name ) - $systemctlIsActive = systemctl is-active $Name 2>&1 + $nameService = Get-PodeRealServiceName -Name $Name + $systemctlIsActive = systemctl is-active $nameService 2>&1 $isActive = $systemctlIsActive -eq 'active' Write-Verbose -Message ($systemctlIsActive -join '`n') return $isActive @@ -733,7 +736,8 @@ function Disable-PodeLinuxService { [string] $Name ) - $systemctlDisable = sudo systemctl disable $Name 2>&1 + $nameService = Get-PodeRealServiceName -Name $Name + $systemctlDisable = sudo systemctl disable $nameService 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($systemctlDisable -join '`n') return $success @@ -793,9 +797,9 @@ function Stop-PodeLinuxService { [string] $Name ) - + $nameService = Get-PodeRealServiceName -Name $Name #return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) - $serviceStopInfo = sudo systemctl stop $("$Name.service".Replace(' ', '_')) 2>&1 + $serviceStopInfo = sudo systemctl stop $nameService 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($serviceStopInfo -join "`n") return $success @@ -825,7 +829,8 @@ function Start-PodeLinuxService { [string] $Name ) - $serviceStartInfo = sudo systemctl start $Name 2>&1 + $nameService = Get-PodeRealServiceName -Name $Name + $serviceStartInfo = sudo systemctl start $nameService 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($serviceStartInfo -join "`n") return $success @@ -855,7 +860,8 @@ function Test-PodeMacOsServiceIsRegistered { [string] $Name ) - $systemctlStatus = launchctl list $Name 2>&1 + $nameService = Get-PodeRealServiceName -Name $Name + $systemctlStatus = launchctl list $nameService 2>&1 $isRegistered = ($LASTEXITCODE -eq 0) Write-Verbose -Message ($systemctlStatus -join '`n') return $isRegistered @@ -967,7 +973,8 @@ function Test-PodeMacOsServiceIsActive { [string] $Name ) - $serviceInfo = launchctl list $name + $nameService = Get-PodeRealServiceName -Name $Name + $serviceInfo = launchctl list $nameService $isActive = $serviceInfo -match '"PID" = (\d+);' Write-Verbose -Message ($serviceInfo -join "`n") return $isActive.Count -eq 1 @@ -1065,10 +1072,6 @@ function Stop-PodeMacOsService { ) return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) - # $serviceStopInfo = launchctl stop $Name 2>&1 - # $success = $LASTEXITCODE -eq 0 - # Write-Verbose -Message ($serviceStopInfo -join "`n") - # return $success } <# @@ -1090,30 +1093,72 @@ function Stop-PodeMacOsService { 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 ) + $nameService = Get-PodeRealServiceName -Name $Name $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) if ($sudo) { - $serviceStartInfo = sudo launchctl start $Name 2>&1 + $serviceStartInfo = sudo launchctl start $nameService 2>&1 } else { - $serviceStartInfo = launchctl start $Name 2>&1 + $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). +.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] @@ -1121,27 +1166,37 @@ function Send-PodeServiceSignal { ) # Standardize service naming for Linux/macOS - $nameService = $(if ($IsMacOS) { "pode.$Name.service".Replace(' ', '_') }else { "$Name.service".Replace(' ', '_') }) + $nameService = Get-PodeRealServiceName + # Map signal names to their corresponding Unix signal numbers $signalMap = @{ - 'SIGTSTP' = 20 - 'SIGCONT' = 18 - 'SIGHUP' = 1 - 'SIGTERM' = 15 + '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 + + # 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'." @@ -1153,8 +1208,320 @@ function Send-PodeServiceSignal { } } else { - # Service is not registered + # 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 -} \ No newline at end of file +} + +<# +.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 across Windows, Linux, and macOS platforms. + +.DESCRIPTION + The `Get-PodeServiceStatus` function retrieves detailed information about a specified Pode service, including its current status, process ID (PID), and whether it requires elevated privileges (`Sudo`). The behavior varies based on the platform: + - Windows: Uses CIM to query the service status and maps standard states to Pode-specific states. + - Linux: Checks service status using `systemctl` and optionally reads additional state information from a custom state file. + - macOS: Uses `launchctl` and custom logic to determine the service's status and PID. + +.PARAMETER Name + The name of the Pode service to query. + +.EXAMPLE + Get-PodeServiceStatus -Name "MyPodeService" + + Retrieves the status of the Pode service named "MyPodeService". + +.OUTPUTS + [hashtable] The function returns a hashtable with the following keys: + - Name: The service name. + - 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. + +.NOTES + - Requires administrative/root privileges to access service information on Linux and macOS. + - Platform-specific behaviors: + - **Windows**: Retrieves service information via the `Win32_Service` class. + - **Linux**: Uses `systemctl` to query the service status and retrieves custom Pode state if available. + - **macOS**: Uses `launchctl` to query service information and checks for custom Pode state files. + - If the service is not found, the function returns `$null`. + - Logs errors and warnings for troubleshooting. + - This is an internal function and may change in future releases of Pode. +#> +function Get-PodeServiceStatus { + [CmdletBinding()] + [OutputType([hashtable])] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + + 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 @{ + Name = $Name + Status = $status + Pid = $service.ProcessId + Sudo = $true + } + } + else { + Write-Verbose -Message "Service '$Name' not found." + return $null + } + } + + elseif ($IsLinux) { + try { + $nameService = Get-PodeRealServiceName + # 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 @{ + Name = $Name + Status = $status + Pid = $servicePid + Sudo = $true + } + } + else { + Write-Verbose -Message "Service '$nameService' not found." + } + } + catch { + $_ | Write-PodeErrorLog + Write-Error -Exception $_.Exception + return $null + } + } + + elseif ($IsMacOS) { + try { + $nameService = Get-PodeRealServiceName + # Check if the service exists on macOS (launchctl) + if ((Test-PodeMacOsServiceIsRegistered $nameService )) { + $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match + + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + + # Check if the service has a PID entry + if ($servicePid -ne 0) { + if ($sudo) { + $stateFilePath = "/Library/LaunchDaemons/PodeMonitor/$servicePid.state" + } + else { + $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" + } + if (Test-Path -Path $stateFilePath) { + $status = Get-Content -Path $stateFilePath + } + return @{ + Name = $Name + Status = $status + Pid = $servicePid + Sudo = $sudo + } + } + else { + return @{ + Name = $Name + Status = 'Stopped' + Pid = 0 + Sudo = $sudo + } + } + } + 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 + ) + + # Standardize service naming for macOS + if ($IsMacOS) { + return "pode.$Name.service".Replace(' ', '_') + } + + # Standardize service naming for Linux + if ($IsLinux) { + return "$Name.service".Replace(' ', '_') + } + + # For Windows, return the name as-is + return $Name +} diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index be973bc68..ea93bb6db 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -266,37 +266,63 @@ function Register-PodeService { } } - <# .SYNOPSIS - Starts a Pode-based service across different platforms (Windows, Linux, and macOS). + Start a Pode-based service on Windows, Linux, or macOS. .DESCRIPTION - The `Start-PodeService` function checks if a Pode-based service is already running, and if not, it starts the service. - It works on Windows, Linux (systemd), and macOS (launchctl), handling platform-specific service commands to start the service. - If the service is not registered, it will throw an error. + 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. + 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. + +.EXAMPLE + Start-PodeService -Name 'MyService' + + Starts the service named 'MyService' if it is not already running. .EXAMPLE - Start-PodeService + Start-PodeService -Name 'MyService' -Async - Starts the Pode-based service if it is not currently running. + Starts the service named 'MyService' and returns immediately. .NOTES - - The function retrieves the service name from the `srvsettings.json` file located in the script directory. - - On Windows, it uses `Get-Service` and `Start-Service` to manage the service. - - On Linux, it uses `systemctl` to manage the service. - - On macOS, it uses `launchctl` to manage the service. - - If the service is already running, no action is taken. - - If the service is not registered, the function throws an error. + - 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 + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] + $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [int] + $Timeout = 10 ) # Ensure the script is running with the necessary administrative/root privileges. # Exits the script if the current user lacks the required privileges. @@ -304,95 +330,59 @@ function Start-PodeService { try { - if (Test-PodeIsWindows) { - - # Get the Windows service - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service) { - # Check if the service is already running - if ($service.Status -eq 'Stopped') { - if( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "start '$Name'"){ - return $true - } + $service = Get-PodeServiceStatus -Name $Name + if (!$service) { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + if ($service.Status -eq 'Running') { + Write-Verbose -Message "Service '$Name' is already Running." + return $true + } + if ($service.Status -ne 'Stopped') { + Write-Verbose -Message "Service '$Name' is not Stopped." + return $false + } - <# $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service.Status -eq 'Running') { - Write-Verbose -Message "Service '$Name' started successfully." - } - else { - #> - throw ($PodeLocale.serviceCommandFailedException -f 'Start-Service', $Name) - # } + if (Test-PodeIsWindows) { + if ( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "start '$Name'") { + if ($Async) { + return $true } else { - # Log service is already running - Write-Verbose -Message "Service '$Name' is $($service.Status)." - return ($service.Status -eq 'Running') + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) - } - } - elseif ($IsLinux) { - $nameService = "$Name.service".Replace(' ', '_') - # Check if the service exists - if ((Test-PodeLinuxServiceIsRegistered $nameService)) { - # Check if the service is already running - if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { - # Start the service - if ((Start-PodeLinuxService -Name $nameService)) { - return $true - # Check if the service is active - <# if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$nameService' started successfully." - return $true - }#> + throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe start {0}', $Name) - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $nameService) + } + elseif ($IsLinux) { + # Start the service + if ((Start-PodeLinuxService -Name $Name)) { + if ($Async) { + return $true } else { - # Log service is already running - Write-Verbose -Message "Service '$nameService' is already running." + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) - } - } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $Name) + } elseif ($IsMacOS) { - $nameService = "pode.$Name.service".Replace(' ', '_') - # Check if the service exists - if ((Test-PodeMacOsServiceIsRegistered $nameService)) { - # Check if the service is already running - if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { - # Start the service - if ((Start-PodeMacOsService -Name $nameService)) { - # Check if the service is active - return $true - <# if ((Test-PodeMacOsServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$nameService' started successfully." - return $true - }#> - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $nameService) + # Start the service + if ((Start-PodeMacOsService -Name $Name)) { + if ($Async) { + return $true } else { - # Log service is already running - Write-Verbose -Message "Service '$nameService' is already running." + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) - } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl start', $Name) } } catch { @@ -405,33 +395,61 @@ function Start-PodeService { <# .SYNOPSIS - Stops a Pode-based service across different platforms (Windows, Linux, and macOS). + Stop a Pode-based service on Windows, Linux, or macOS. .DESCRIPTION - The `Stop-PodeService` function stops a Pode-based service by checking if it is currently running. - If the service is running, it will attempt to stop the service gracefully. - The function works on Windows, Linux (systemd), and macOS (launchctl). + 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. + 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. .EXAMPLE - Stop-PodeService + Stop-PodeService -Name 'MyService' - Stops the Pode-based service if it is currently running. If the service is not running, no action is taken. + 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 - - The function retrieves the service name from the `srvsettings.json` file located in the script directory. - - On Windows, it uses `Get-Service` and `Stop-Service`. - - On Linux, it uses `systemctl` to stop the service. - - On macOS, it uses `launchctl` to stop the service. - - If the service is not registered, the function throws an error. + - 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 + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] + $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [int] + $Timeout = 10 ) try { # Ensure the script is running with the necessary administrative/root privileges. @@ -444,19 +462,16 @@ function Stop-PodeService { if ($service) { # Check if the service is running if ($service.Status -eq 'Running' -or $service.Status -eq 'Paused') { - if( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "stop '$Name'") - { - return $true - } - <# $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service.Status -eq 'Stopped') { - Write-Verbose -Message "Service '$Name' stopped successfully." - } - else { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) + if ( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "stop '$Name'") { + if ($Async) { + return $true + } + else { + return Wait-PodeServiceStatus -Name $Name -Status Stopped -Timeout $Timeout + } } - #> + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe stop', $Name) } else { Write-Verbose -Message "Service '$Name' is not running." @@ -468,66 +483,32 @@ function Stop-PodeService { } } elseif ($IsLinux) { - $nameService = "$Name.service".Replace(' ', '_') - # Check if the service is already registered - if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { - # Check if the service is active - if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { - #Stop the service - if (( Stop-PodeLinuxService -Name $Name)) { - return $true - # Check if the service is active - <# if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - return $true - }#> - - } - - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo launchctl stop', $Name) - + #Stop the service + if (( Stop-PodeLinuxService -Name $Name)) { + if ($Async) { + return $true } else { - Write-Verbose -Message "Service '$Name' is not running." + return Wait-PodeServiceStatus -Name $Name -Status Stopped -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService) - } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl stop', $Name) } elseif ($IsMacOS) { - $nameService = "pode.$Name.service".Replace(' ', '_') - # Check if the service is already registered - if ((Test-PodeMacOsServiceIsRegistered -Name $nameService)) { - # Check if the service is active - if ((Test-PodeMacOsServiceIsActive $nameService)) { - if ((Stop-PodeMacOsService $Name)) { - return $true - <# for ($i = 0; $i -lt 30; $i++) { - if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - return $true - } - Start-Sleep 1 - } - }#> - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) - + if ((Stop-PodeMacOsService $Name)) { + if ($Async) { + return $true } else { - Write-Verbose -Message "Service '$Name' is not running." + return Wait-PodeServiceStatus -Name $Name -Status Stopped -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) - } + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) } + } catch { $_ | Write-PodeErrorLog @@ -539,61 +520,109 @@ function Stop-PodeService { <# .SYNOPSIS - Suspends a specified service on Windows systems. + Suspend a specified service on Windows systems. .DESCRIPTION - This function attempts to suspend a service by name. It is supported only on Windows systems. - On Linux and macOS, the suspend functionality for services is not available and an appropriate error message is returned. + 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. + 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. .EXAMPLE - Suspend-PodeService -Name 'MyService' + Suspend-PodeService -Name 'MyService' + + Suspends the service named 'MyService' if it is currently running. .NOTES - - This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. - - This function supports Windows only. + - 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 + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] + $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [int] + $Timeout = 10 ) + # 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 { - # Ensure the script is running with the necessary administrative/root privileges. - # Exits the script if the current user lacks the required privileges. - Confirm-PodeAdminPrivilege + $service = Get-PodeServiceStatus -Name $Name + if (!$service) { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + if ($service.Status -eq 'Suspended') { + Write-Verbose -Message "Service '$Name' is already suspended." + return $true + } + if ($service.Status -ne 'Running') { + Write-Verbose -Message "Service '$Name' is not running." + return $false + } if (Test-PodeIsWindows) { - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service) { - # Check if the service is running - if ($service.Status -eq 'Running') { - $null = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "pause '$Name'" - - <# $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service.Status -eq 'Paused') { - Write-Verbose -Message "Service '$Name' suspended successfully." - } - else { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'Suspend-Service', $Name) - }#> + if (( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "pause '$Name'")) { + if ($Async) { + return $true } else { - Write-Verbose -Message "Service '$Name' is not running." - return $false + return Wait-PodeServiceStatus -Name $Name -Status Suspended -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe pause', $Name) + } + elseif ($IsLinux) { + if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP')) { + if ($Async) { + return $true + } + else { + return Wait-PodeServiceStatus -Name $Name -Status Paused -Timeout $Timeout + } } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f ' sudo /bin/kill -SIGTSTP', $Name) } - elseif ($IsLinux -or $IsMacOS) { - return Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP' + elseif ($IsMacOS) { + if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP')) { + if ($Async) { + return $true + } + else { + return Wait-PodeServiceStatus -Name $Name -Status Paused -Timeout $Timeout + } + } + + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f '/bin/kill -SIGTSTP ', $Name) } } catch { @@ -606,63 +635,108 @@ function Suspend-PodeService { <# .SYNOPSIS - Resumes a specified service on Windows systems. + Resume a specified service on Windows systems. .DESCRIPTION - This function attempts to resume a service by name. It is supported only on Windows systems. - On Linux and macOS, the resume functionality for services is not available, and an appropriate error message is returned. + 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. + 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. .EXAMPLE - Resume-PodeService -Name 'MyService' + Resume-PodeService -Name 'MyService' + + Resumes the service named 'MyService' if it is currently paused. .NOTES - - This function requires administrative/root privileges to execute. On non-Windows platforms, an error is logged indicating that this feature is not supported. - - This function supports Windows only. + - 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 + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] + $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [int] + $Timeout = 10 ) + # 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 { - # Ensure the script is running with the necessary administrative/root privileges. - # Exits the script if the current user lacks the required privileges. - Confirm-PodeAdminPrivilege - if (Test-PodeIsWindows) { - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service) { - # Check if the service is running - if ($service.Status -eq 'Paused') { - $null = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "continue '$Name'" - <# - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service.Status -eq 'Running') { - Write-Verbose -Message "Service '$Name' Resumed successfully." - } - else { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'Resume-Service', $Name) - } - #> + $service = Get-PodeServiceStatus -Name $Name + if (!$service) { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + + if ($service.Status -ne 'Paused') { + Write-Verbose -Message "Service '$Name' is not Suspended." + return $false + } + if (Test-PodeIsWindows) { + if (( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "continue '$Name'")) { + if ($Async) { + return $true } else { - Write-Verbose -Message "Service '$Name' is not suspended." - return $false + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + # Service command '{0}' failed on service '{1}'. + throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe continue', $Name) + } + elseif ($IsLinux) { + if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT')) { + 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 /bin/kill -SIGCONT', $Name) } - elseif ($IsLinux -or $IsMacOS) { - return Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' + elseif ($IsMacOS) { + if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT')) { + 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 '/bin/kill -SIGCONT ', $Name) } + } catch { $_ | Write-PodeErrorLog @@ -742,7 +816,7 @@ function Unregister-PodeService { } else { # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f $nameService ) + throw ($Podelocale.serviceIsRunningException -f $Name ) } } @@ -774,16 +848,15 @@ function Unregister-PodeService { elseif ($IsLinux) { try { - $nameService = "$Name.service".Replace(' ', '_') # Check if the service is already registered - if ((Test-PodeLinuxServiceIsRegistered $nameService)) { + if ((Test-PodeLinuxServiceIsRegistered $Name)) { # Check if the service is active - if ((Test-PodeLinuxServiceIsActive -Name $nameService)) { + if ((Test-PodeLinuxServiceIsActive -Name $Name)) { if ($Force.IsPresent) { #Stop the service if (( Stop-PodeLinuxService -Name $Name)) { # Check if the service is active - if (!(Test-PodeLinuxServiceIsActive -Name $nameService)) { + if (!(Test-PodeLinuxServiceIsActive -Name $Name)) { Write-Verbose -Message "Service '$Name' stopped successfully." } else { @@ -798,12 +871,12 @@ function Unregister-PodeService { } else { # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f $nameService) + throw ($Podelocale.serviceIsRunningException -f $Name) } } - if ((Disable-PodeLinuxService -Name $nameService)) { + if ((Disable-PodeLinuxService -Name $Name)) { # Read the content of the service file - $serviceFilePath = "/etc/systemd/system/$nameService" + $serviceFilePath = "/etc/systemd/system/$(Get-PodeRealServiceName -Name $Name)" if ((Test-path -path $serviceFilePath -PathType Leaf)) { $serviceFileContent = sudo cat $serviceFilePath @@ -829,7 +902,7 @@ function Unregister-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name ) } return $true } @@ -842,16 +915,15 @@ function Unregister-PodeService { elseif ($IsMacOS) { try { - $nameService = "pode.$Name.service".Replace(' ', '_') # Check if the service is already registered - if (Test-PodeMacOsServiceIsRegistered $nameService) { + if (Test-PodeMacOsServiceIsRegistered $Name) { # Check if the service is active - if ((Test-PodeMacOsServiceIsActive -Name $nameService)) { + if ((Test-PodeMacOsServiceIsActive -Name $Name)) { if ($Force.IsPresent) { #Stop the service if (( Stop-PodeMacOsService -Name $Name)) { # Check if the service is active - if (!(Test-PodeMacOsServiceIsActive -Name $nameService)) { + if (!(Test-PodeMacOsServiceIsActive -Name $Name)) { Write-Verbose -Message "Service '$Name' stopped successfully." } else { @@ -866,17 +938,17 @@ function Unregister-PodeService { } else { # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f $nameService) + throw ($Podelocale.serviceIsRunningException -f $Name) } } - if ((Disable-PodeMacOsService -Name $nameService)) { - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + if ((Disable-PodeMacOsService -Name $Name)) { + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$(Get-PodeRealServiceName -Name $Name).plist" -PathType Leaf) if ($sudo) { - $plistFilePath = "/Library/LaunchDaemons/$nameService.plist" + $plistFilePath = "/Library/LaunchDaemons/$(Get-PodeRealServiceName -Name $Name).plist" } else { - $plistFilePath = "$HOME/Library/LaunchAgents/$nameService.plist" + $plistFilePath = "$HOME/Library/LaunchAgents/$(Get-PodeRealServiceName -Name $Name).plist" } #Check if the plist file exists if (Test-Path -Path $plistFilePath) { @@ -902,7 +974,7 @@ function Unregister-PodeService { } else { # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $nameService ) + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name ) } return $true } @@ -957,6 +1029,8 @@ function Unregister-PodeService { - For macOS, it uses `launchctl` to check if the service is running. #> function Get-PodeService { + [CmdletBinding()] + [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] [string] @@ -965,216 +1039,121 @@ function Get-PodeService { # Ensure the script is running with the necessary administrative/root privileges. # Exits the script if the current user lacks the required privileges. Confirm-PodeAdminPrivilege - - 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 @{ - Name = $Name - Status = $status - Pid = $service.ProcessId - Sudo = $true - } - } - else { - Write-Verbose -Message "Service '$Name' not found." - return $null - } - } - - elseif ($IsLinux) { - try { - $nameService = "$Name.service".Replace(' ', '_') - # 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 @{ - Name = $Name - Status = $status - Pid = $servicePid - Sudo = $true - } - } - else { - Write-Verbose -Message "Service '$nameService' not found." - } - } - catch { - $_ | Write-PodeErrorLog - Write-Error -Exception $_.Exception - return $null - } - } - - elseif ($IsMacOS) { - try { - $nameService = "pode.$Name.service".Replace(' ', '_') - # Check if the service exists on macOS (launchctl) - if ((Test-PodeMacOsServiceIsRegistered $nameService )) { - $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match - - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) - - # Check if the service has a PID entry - if ($servicePid -ne 0) { - if ($sudo) { - $stateFilePath = "/Library/LaunchDaemons/PodeMonitor/$servicePid.state" - } - else { - $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" - } - if (Test-Path -Path $stateFilePath) { - $status = Get-Content -Path $stateFilePath - } - return @{ - Name = $Name - Status = $status - Pid = $servicePid - Sudo = $sudo - } - } - else { - return @{ - Name = $Name - Status = 'Stopped' - Pid = 0 - Sudo = $sudo - } - } - } - else { - Write-Verbose -Message "Service '$Name' not found." - return $null - } - } - catch { - $_ | Write-PodeErrorLog - Write-Error -Exception $_.Exception - return $null - } - } + return Get-PodeServiceStatus -Name $Name } <# .SYNOPSIS - Restarts a Pode service on Windows, Linux, or macOS by sending the appropriate restart signal. + Restart a Pode service on Windows, Linux, or macOS by sending the appropriate restart signal. .DESCRIPTION - This function handles the restart operation for a Pode service across multiple platforms: - - On Windows: Sends a restart control signal (128) using `sc control`. - - On Linux and macOS: Sends the `SIGHUP` signal to the service's process ID. + 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. + The name of the Pode service to restart. -.NOTES - Requires administrative/root privileges to execute service operations. - - This function leverages platform-specific methods: - - Windows: Uses `sc control` for service control commands. - - Linux/macOS: Uses `/bin/kill -SIGHUP` to signal the service's process. +.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. - For services not running, a verbose message is displayed, and no restart signal is sent. +.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. .EXAMPLE - Restart-PodeService -Name "MyPodeService" + Restart-PodeService -Name "MyPodeService" - Attempts to restart the Pode service named "MyPodeService" on the current platform. + Attempts to restart the Pode service named "MyPodeService" on the current platform. .EXAMPLE - Restart-PodeService -Name "AnotherService" -Verbose + Restart-PodeService -Name "AnotherService" -Verbose - Restarts the Pode service named "AnotherService" with detailed verbose output. + 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 + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Async')] + [switch] + $Async, + + [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [int] + $Timeout = 10 ) + 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 { - # Ensure the script is running with the necessary administrative/root privileges. - # Exits the script if the current user lacks the required privileges. - Confirm-PodeAdminPrivilege - Write-Verbose -Message "Attempting to restart service '$Name' on platform $([System.Environment]::OSVersion.Platform)..." + $service = Get-PodeServiceStatus -Name $Name + if (!$service) { + # Service is not registered + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } + if (@('Running', 'Suspended' ) -inotcontains $service.Status ) { + Write-Verbose -Message "Service '$Name' is not Running or Suspended." + return $false + } if (Test-PodeIsWindows) { - # Handle Windows-specific restart logic - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service) { - if ($service.Status -eq 'Running' -or $service.Status -eq 'Paused') { - Write-Verbose -Message "Sending restart (128) signal to service '$Name'." - $null = Invoke-PodeWinElevatedCommand -Command 'sc control' -Arguments "'$Name' 128" - Start-Sleep 5 - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - Write-Verbose -Message "Service '$Name' restart signal sent successfully." + + Write-Verbose -Message "Sending restart (128) signal to service '$Name'." + if ( Invoke-PodeWinElevatedCommand -Command 'sc control' -Arguments "'$Name' 128") { + if ($Async) { + return $true } else { - Write-Verbose -Message "Service '$Name' is not running." - return $false + return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + 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 ($IsLinux -or $IsMacOS) { - if ( !(Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP')) { - Write-Verbose -Message "Service '$Name' is not running." - return $false + elseif ($IsMacOS) { + # 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) } } catch { From 7f77d94de61ad6afe12b2f54a035748378aaed99 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Nov 2024 17:40:39 -0800 Subject: [PATCH 66/93] fix windows test --- tests/unit/Service.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 index ad61e0cf3..b5d03f700 100644 --- a/tests/unit/Service.Tests.ps1 +++ b/tests/unit/Service.Tests.ps1 @@ -114,13 +114,14 @@ Describe 'Start-PodeService' { 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-Service -MockWith { + Mock -CommandName Get-PodeServiceStatus -MockWith { if ($script:status -eq 'none') { $script:status = 'Stopped' } @@ -140,7 +141,7 @@ Describe 'Start-PodeService' { It 'Starts a started service ' -Skip:(!$IsWindows) { Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { $null } - Mock -CommandName Get-Service -MockWith { + Mock -CommandName Get-PodeServiceStatus -MockWith { [pscustomobject]@{ Name = 'TestService'; Status = 'Running' } } From 035cb12f51a2ade7acad6728344e9a4626529a04 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Mon, 25 Nov 2024 17:58:12 -0800 Subject: [PATCH 67/93] linux fixes --- src/Private/Service.ps1 | 10 +++++----- tests/unit/Service.Tests.ps1 | 27 +++++++++++++++------------ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 9a73bfa80..6524a6c7f 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -244,7 +244,7 @@ function Register-PodeMacService { $Agent ) - $nameService = Get-PodeRealServiceName + $nameService = Get-PodeRealServiceName -Name $Name # Check if the service is already registered if ((Test-PodeMacOsServiceIsRegistered $nameService)) { @@ -410,7 +410,7 @@ function Register-PodeLinuxService { [string] $OsArchitecture ) - $nameService = Get-PodeRealServiceName + $nameService = Get-PodeRealServiceName -Name $Name $null = systemctl status $nameService 2>&1 # Check if the service is already registered @@ -1166,7 +1166,7 @@ function Send-PodeServiceSignal { ) # Standardize service naming for Linux/macOS - $nameService = Get-PodeRealServiceName + $nameService = Get-PodeRealServiceName -Name $Name # Map signal names to their corresponding Unix signal numbers $signalMap = @{ @@ -1372,7 +1372,7 @@ function Get-PodeServiceStatus { elseif ($IsLinux) { try { - $nameService = Get-PodeRealServiceName + $nameService = Get-PodeRealServiceName -Name $Name # Check if the service exists on Linux (systemd) if ((Test-PodeLinuxServiceIsRegistered -Name $nameService)) { $servicePid = 0 @@ -1432,7 +1432,7 @@ function Get-PodeServiceStatus { elseif ($IsMacOS) { try { - $nameService = Get-PodeRealServiceName + $nameService = Get-PodeRealServiceName -Name $Name # Check if the service exists on macOS (launchctl) if ((Test-PodeMacOsServiceIsRegistered $nameService )) { $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 index b5d03f700..92b7ba307 100644 --- a/tests/unit/Service.Tests.ps1 +++ b/tests/unit/Service.Tests.ps1 @@ -114,7 +114,7 @@ Describe 'Start-PodeService' { Mock -CommandName Start-PodeMacOsService Mock -CommandName Write-PodeErrorLog Mock -CommandName Write-Error - Mock -CommandName Get-PodeServiceStatus {return @{Status=''}} + Mock -CommandName Get-PodeServiceStatus { return @{Status = '' } } } Context 'On Windows platform' { @@ -130,7 +130,8 @@ Describe 'Start-PodeService' { } [pscustomobject]@{ Name = 'TestService'; Status = $status } } - Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { return $true } + Mock -CommandName Wait-PodeServiceStatus { $true } + Mock -CommandName Invoke-PodeWinElevatedCommand -MockWith { $true } # Act Start-PodeService -Name 'TestService' | Should -Be $true @@ -161,17 +162,17 @@ Describe 'Start-PodeService' { Context 'On Linux platform' { It 'Starts a stopped service successfully' -Skip:(!$IsLinux) { - $script:status = $null - Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { - if ($null -eq $script:status ) { - $script:status = $false + $script:status = 'none' + Mock -CommandName Get-PodeServiceStatus -MockWith { + if ($script:status -eq 'none') { + $script:status = 'Stopped' } else { - $script:status = $true + $script:status = 'Running' } - return $script:status + [pscustomobject]@{ Name = 'TestService'; Status = $status } } - + Mock -CommandName Wait-PodeServiceStatus { $true } Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $true } Mock -CommandName Start-PodeLinuxService -MockWith { $true } @@ -183,10 +184,12 @@ Describe 'Start-PodeService' { } It 'Starts a started service ' -Skip:(!$IsLinux) { - - Mock -CommandName Test-PodeLinuxServiceIsActive -MockWith { $true } - Mock -CommandName Test-PodeLinuxServiceIsRegistered -MockWith { $true } + Mock -CommandName Start-PodeLinuxService -MockWith { $true } + Mock -CommandName Get-PodeServiceStatus -MockWith { + [pscustomobject]@{ Name = 'TestService'; Status = 'Running' } + } + # Act Start-PodeService -Name 'TestService' | Should -Be $true From bc36b0f6b8270973f31adb6683c6cbd07c67a680 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Mon, 25 Nov 2024 21:27:32 -0800 Subject: [PATCH 68/93] test fixes --- src/Private/Service.ps1 | 20 ++++++++++++-------- src/Public/Service.ps1 | 6 +++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 6524a6c7f..53e256795 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -1166,7 +1166,7 @@ function Send-PodeServiceSignal { ) # Standardize service naming for Linux/macOS - $nameService = Get-PodeRealServiceName -Name $Name + $nameService = Get-PodeRealServiceName -Name $Name # Map signal names to their corresponding Unix signal numbers $signalMap = @{ @@ -1512,16 +1512,20 @@ function Get-PodeRealServiceName { $Name ) - # Standardize service naming for macOS + # 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(' ', '_') } - - # Standardize service naming for Linux - if ($IsLinux) { + elseif ($IsLinux) { return "$Name.service".Replace(' ', '_') } - - # For Windows, return the name as-is - return $Name + else { + # Assume Windows or unknown platform + return $Name + } } diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index ea93bb6db..22d46ee21 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -604,7 +604,7 @@ function Suspend-PodeService { return $true } else { - return Wait-PodeServiceStatus -Name $Name -Status Paused -Timeout $Timeout + return Wait-PodeServiceStatus -Name $Name -Status Suspended -Timeout $Timeout } } @@ -617,7 +617,7 @@ function Suspend-PodeService { return $true } else { - return Wait-PodeServiceStatus -Name $Name -Status Paused -Timeout $Timeout + return Wait-PodeServiceStatus -Name $Name -Status Suspended -Timeout $Timeout } } @@ -694,7 +694,7 @@ function Resume-PodeService { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } - if ($service.Status -ne 'Paused') { + if ($service.Status -ne 'Suspended') { Write-Verbose -Message "Service '$Name' is not Suspended." return $false } From 79e5b2f9b0d116176a40a5edfa9a3a8a936af4aa Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 26 Nov 2024 15:56:33 -0800 Subject: [PATCH 69/93] fix MacOS --- examples/HelloService/HelloService.ps1 | 56 ++++++++++++-------- src/Private/Service.ps1 | 71 ++++++++++++++++++-------- src/Public/Service.ps1 | 13 +++-- 3 files changed, 93 insertions(+), 47 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 69ac3570a..996bbc55a 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -1,11 +1,10 @@ <# .SYNOPSIS - PowerShell script to register, start, stop, query, and unregister a Pode service, with a basic server setup. + PowerShell script to register, manage, and set up a Pode service named '$ServiceName'. .DESCRIPTION - This script manages a Pode service named 'Hello Service' with commands to register, start, stop, query, - and unregister the service. Additionally, it sets up a Pode server that listens on port 8080 and includes - a simple GET route that responds with 'Hello, Service!'. + 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. @@ -13,35 +12,41 @@ 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 'Hello Service' with Pode. + 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 Daemon + Defines the service as an Daemon instead of a Agent.(macOS only) + .PARAMETER Unregister - Unregisters the 'Hello Service' from Pode. Use with the -Force switch to forcefully unregister the service. + 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 'Hello Service'. + Starts the $ServiceName. .PARAMETER Stop - Stops the 'Hello Service'. + Stops the $ServiceName. .PARAMETER Query - Queries the status of the 'Hello Service'. + Queries the status of the $ServiceName. .PARAMETER Suspend - Suspend the 'Hello Service'. + Suspends the $ServiceName. .PARAMETER Resume - Resume the 'Hello Service'. + Resumes the $ServiceName. .PARAMETER Restart - Restart the 'Hello Service'. + Restarts the $ServiceName. .EXAMPLE Register the service: @@ -64,7 +69,7 @@ ./HelloService.ps1 -Unregister -Force .LINK - https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloService.ps1 + https://github.com/Badgerati/Pode/blob/develop/examples/HelloService/HelloService.ps1 .NOTES Author: Pode Team @@ -77,6 +82,10 @@ param( [int] $Port = 8080, + [Parameter( ParameterSetName = 'Inbuilt')] + [string] + $ServiceName = 'Hello Service', + [Parameter(Mandatory = $true, ParameterSetName = 'Register')] [switch] $Register, @@ -85,6 +94,10 @@ param( [securestring] $Password, + [Parameter(ParameterSetName = 'Register')] + [switch] + $Daemon, + [Parameter(Mandatory = $true, ParameterSetName = 'Unregister')] [switch] $Unregister, @@ -116,7 +129,6 @@ param( [Parameter( ParameterSetName = 'Restart')] [switch] $Restart - ) try { # Get the path of the script being executed @@ -141,40 +153,40 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name 'Hello Service' -ParameterString "-Port $Port" -Password $Password -Agent + Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:!$Daemon exit } if ( $Unregister.IsPresent) { - Unregister-PodeService -Name 'Hello Service' -Force:$Force + Unregister-PodeService -Name $ServiceName -Force:$Force exit } if ($Start.IsPresent) { - Start-PodeService -Name 'Hello Service' + Start-PodeService -Name $ServiceName exit } if ($Stop.IsPresent) { - Stop-PodeService -Name 'Hello Service' + Stop-PodeService -Name $ServiceName exit } if ($Suspend.IsPresent) { - Suspend-PodeService -Name 'Hello Service' + Suspend-PodeService -Name $ServiceName exit } if ($Resume.IsPresent) { - Resume-PodeService -Name 'Hello Service' + Resume-PodeService -Name $ServiceName exit } if ($Query.IsPresent) { - Get-PodeService -Name 'Hello Service' + Get-PodeService -Name $ServiceName exit } if ($Restart.IsPresent) { - Restart-PodeService -Name 'Hello Service' + Restart-PodeService -Name $ServiceName exit } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 53e256795..5da16e39f 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -254,12 +254,11 @@ function Register-PodeMacService { # Determine whether the service should run at load $runAtLoad = if ($Autostart.IsPresent) { '' } else { '' } - if ($Agent) { - $plistPath = "$($HOME)/Library/LaunchAgents/$($nameService).plist" - } - else { - $plistPath = "/Library/LaunchDaemons/$($nameService).plist" - } + + + # Create a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + # Create the plist content @" @@ -299,20 +298,31 @@ function Register-PodeMacService { --> -"@ | Set-Content -Path $plistPath -Encoding UTF8 +"@ | Set-Content -Path $tempFile -Encoding UTF8 Write-Verbose -Message "Service '$nameService' WorkingDirectory : $($BinPath)." - - chmod +r $plistPath - try { - # Load the plist with launchctl 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 { - sudo launchctl load $plistPat + $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)) { # Service registration failed. @@ -322,7 +332,6 @@ function Register-PodeMacService { catch { $_ | Write-PodeErrorLog throw $_ # Rethrow the error after logging - return $false } return $true @@ -590,7 +599,6 @@ function Register-PodeMonitorWindowsService { catch { $_ | Write-PodeErrorLog throw $_ # Rethrow the error after logging - return $false } return $true @@ -861,7 +869,13 @@ function Test-PodeMacOsServiceIsRegistered { $Name ) $nameService = Get-PodeRealServiceName -Name $Name - $systemctlStatus = launchctl list $nameService 2>&1 + $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 @@ -974,7 +988,13 @@ function Test-PodeMacOsServiceIsActive { $Name ) $nameService = Get-PodeRealServiceName -Name $Name - $serviceInfo = launchctl list $nameService + $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 @@ -1004,7 +1024,14 @@ function Get-PodeMacOsServicePid { [string] $Name ) - $serviceInfo = launchctl list $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 }) @@ -1034,12 +1061,14 @@ function Disable-PodeMacOsService { [string] $Name ) - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($Name).plist" -PathType Leaf) + # Standardize service naming for Linux/macOS + $nameService = Get-PodeRealServiceName -Name $Name + $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) if ($sudo) { - $systemctlDisable = sudo launchctl unload "/Library/LaunchDaemons/$Name.plist" 2>&1 + $systemctlDisable = sudo launchctl unload "/Library/LaunchDaemons/$nameService.plist" 2>&1 } else { - $systemctlDisable = launchctl unload "$HOME/Library/LaunchAgents/$Name.plist" 2>&1 + $systemctlDisable = launchctl unload "$HOME/Library/LaunchAgents/$nameService.plist" 2>&1 } $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($systemctlDisable -join '`n') @@ -1516,7 +1545,7 @@ function Get-PodeRealServiceName { if ($Name -like '*.service') { return $Name } - + # Standardize service naming based on platform if ($IsMacOS) { return "pode.$Name.service".Replace(' ', '_') diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 22d46ee21..b95907832 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -957,12 +957,17 @@ function Unregister-PodeService { # Extract the second string in the ProgramArguments array (the settings file path) $settingsFile = $plistXml.plist.dict.array.string[1] - - if ((Test-Path -Path $settingsFile -PathType Leaf)) { - Remove-Item -Path $settingsFile + if ($sudo) { + sudo rm $settingsFile + sudo rm $plistFilePath } + else { + if ((Test-Path -Path $settingsFile -PathType Leaf)) { + Remove-Item -Path $settingsFile + } - Remove-Item -Path $plistFilePath -ErrorAction Break + Remove-Item -Path $plistFilePath -ErrorAction Break + } Write-Verbose -Message "Service '$Name' unregistered successfully." } From 6f817e6d3429cae3e57b910a63e603b4076983bc Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 26 Nov 2024 16:17:48 -0800 Subject: [PATCH 70/93] fixes --- examples/HelloService/HelloService.ps1 | 2 +- src/Private/Service.ps1 | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 996bbc55a..904bec2ae 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -153,7 +153,7 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:!$Daemon + Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:(!$Daemon.IsPresent) exit } if ( $Unregister.IsPresent) { diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 5da16e39f..cb22cbf40 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -247,7 +247,7 @@ function Register-PodeMacService { $nameService = Get-PodeRealServiceName -Name $Name # Check if the service is already registered - if ((Test-PodeMacOsServiceIsRegistered $nameService)) { + if ((Test-PodeMacOsServiceIsRegistered $nameService -Agent:$Agent)) { # Service is already registered. throw ($PodeLocale.serviceAlreadyRegisteredException -f $nameService) } @@ -324,7 +324,7 @@ function Register-PodeMacService { } # Verify the service is now registered - if (! (Test-PodeMacOsServiceIsRegistered $nameService)) { + if (! (Test-PodeMacOsServiceIsRegistered $nameService -Agent:$Agent)) { # Service registration failed. throw ($PodeLocale.serviceRegistrationException -f $nameService) } @@ -866,10 +866,18 @@ function Test-PodeMacOsServiceIsRegistered { param( [Parameter(Mandatory = $true)] [string] - $Name + $Name, + + [switch] + $Agent ) $nameService = Get-PodeRealServiceName -Name $Name - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) + 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 } From 10a272d25033f0f77af9fc6d1be436873fea4e08 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 26 Nov 2024 17:49:17 -0800 Subject: [PATCH 71/93] fix MacOS test (I hope) --- tests/integration/Service.Tests.ps1 | 14 +++++++------- tests/unit/Service.Tests.ps1 | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index fc07eb65e..78e91a8f9 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -8,7 +8,7 @@ Describe 'Service Lifecycle' { it 'register' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register - Start-Sleep 10 + Start-Sleep 2 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query if ($IsMacOS) { $status.Status | Should -Be 'Running' @@ -26,7 +26,7 @@ Describe 'Service Lifecycle' { it 'start' -Skip:( $IsMacOS) { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start - Start-Sleep 10 + Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -37,7 +37,7 @@ Describe 'Service Lifecycle' { it 'pause' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend - Start-Sleep 10 + Start-Sleep 2 # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Suspended' @@ -48,7 +48,7 @@ Describe 'Service Lifecycle' { it 'resume' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume - Start-Sleep 10 + Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -58,7 +58,7 @@ Describe 'Service Lifecycle' { } it 'stop' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - Start-Sleep 10 + Start-Sleep 2 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Stopped' $status.Name | Should -Be 'Hello Service' @@ -69,7 +69,7 @@ Describe 'Service Lifecycle' { it 're-start' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start - Start-Sleep 10 + Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' @@ -81,7 +81,7 @@ Describe 'Service Lifecycle' { it 're-stop' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - Start-Sleep 10 + Start-Sleep 2 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query diff --git a/tests/unit/Service.Tests.ps1 b/tests/unit/Service.Tests.ps1 index 92b7ba307..a198585e4 100644 --- a/tests/unit/Service.Tests.ps1 +++ b/tests/unit/Service.Tests.ps1 @@ -184,7 +184,7 @@ Describe 'Start-PodeService' { } It 'Starts a started service ' -Skip:(!$IsLinux) { - + Mock -CommandName Start-PodeLinuxService -MockWith { $true } Mock -CommandName Get-PodeServiceStatus -MockWith { [pscustomobject]@{ Name = 'TestService'; Status = 'Running' } @@ -206,19 +206,19 @@ Describe 'Start-PodeService' { Context 'On macOS platform' { It 'Starts a stopped service successfully' -Skip:(!$IsMacOS) { - Mock -CommandName Test-PodeMacOsServiceIsRegistered -MockWith { $true } - Mock -CommandName Start-PodeMacOsService -MockWith { $true } - - $script:status = $null - Mock -CommandName Test-PodeMacOsServiceIsActive -MockWith { - if ($null -eq $script:status ) { - $script:status = $false + $script:status = 'none' + Mock -CommandName Get-PodeServiceStatus -MockWith { + if ($script:status -eq 'none') { + $script:status = 'Stopped' } else { - $script:status = $true + $script:status = 'Running' } - return $script:status + [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 From 0c50dd1c96378d838aca2b4a917fa9945ca2b712 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 26 Nov 2024 21:20:12 -0800 Subject: [PATCH 72/93] Retry --- src/Private/Service.ps1 | 3 ++- src/Public/Service.ps1 | 34 +++++------------------------ tests/integration/Service.Tests.ps1 | 2 +- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index cb22cbf40..195b14fd6 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -1485,7 +1485,8 @@ function Get-PodeServiceStatus { $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" } if (Test-Path -Path $stateFilePath) { - $status = Get-Content -Path $stateFilePath + $status = Get-Content -Path $stateFilePath -Raw + $status = $status.Substring(0, 1).ToUpper() + $status.Substring(1) } return @{ Name = $Name diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index b95907832..a5ab332b5 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -804,12 +804,8 @@ function Unregister-PodeService { # Check if the service is running before attempting to stop it if ($service.Status -eq 'Running') { if ($Force.IsPresent) { - $null = Invoke-PodeWinElevatedCommand -Command 'Stop-Service' -Arguments "-Name '$Name'" - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service.Status -eq 'Stopped') { - Write-Verbose -Message "Service '$Name' stopped forcefully." - } - else { + $null = Invoke-PodeWinElevatedCommand -Command 'sc' -Arguments "stop '$Name'" + if (!( Stop-PodeService -Name $Name)) { # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) } @@ -854,17 +850,7 @@ function Unregister-PodeService { if ((Test-PodeLinuxServiceIsActive -Name $Name)) { if ($Force.IsPresent) { #Stop the service - if (( Stop-PodeLinuxService -Name $Name)) { - # Check if the service is active - if (!(Test-PodeLinuxServiceIsActive -Name $Name)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - } - else { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl stop', $Name) - } - } - else { + if (!( Stop-PodeService -Name $Name)) { # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl stop', $Name) } @@ -921,17 +907,7 @@ function Unregister-PodeService { if ((Test-PodeMacOsServiceIsActive -Name $Name)) { if ($Force.IsPresent) { #Stop the service - if (( Stop-PodeMacOsService -Name $Name)) { - # Check if the service is active - if (!(Test-PodeMacOsServiceIsActive -Name $Name)) { - Write-Verbose -Message "Service '$Name' stopped successfully." - } - else { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) - } - } - else { + if (!( Stop-PodeService -Name $Name)) { # Service command '{0}' failed on service '{1}'. throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) } @@ -948,7 +924,7 @@ function Unregister-PodeService { $plistFilePath = "/Library/LaunchDaemons/$(Get-PodeRealServiceName -Name $Name).plist" } else { - $plistFilePath = "$HOME/Library/LaunchAgents/$(Get-PodeRealServiceName -Name $Name).plist" + $plistFilePath = "$($HOME)/Library/LaunchAgents/$(Get-PodeRealServiceName -Name $Name).plist" } #Check if the plist file exists if (Test-Path -Path $plistFilePath) { diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 78e91a8f9..63693366e 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -8,7 +8,7 @@ Describe 'Service Lifecycle' { it 'register' { . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register - Start-Sleep 2 + Start-Sleep 10 $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query if ($IsMacOS) { $status.Status | Should -Be 'Running' From c9c84f3d3efa00d8b8150eb3025703cf42ebaaab Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 27 Nov 2024 10:10:58 -0800 Subject: [PATCH 73/93] Code Improvements --- examples/HelloService/HelloService.ps1 | 24 +- src/Pode.psm1 | 9 + src/Private/Service.ps1 | 115 ++--- src/Public/Service.ps1 | 653 +++++++++++-------------- tests/integration/Service.Tests.ps1 | 43 +- 5 files changed, 393 insertions(+), 451 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 904bec2ae..5ed6ccf76 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -153,41 +153,33 @@ catch { if ( $Register.IsPresent) { - Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:(!$Daemon.IsPresent) - exit + return Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:(!$Daemon.IsPresent) } if ( $Unregister.IsPresent) { - Unregister-PodeService -Name $ServiceName -Force:$Force - exit + return Unregister-PodeService -Name $ServiceName -Force:$Force } if ($Start.IsPresent) { - Start-PodeService -Name $ServiceName - exit + return Start-PodeService -Name $ServiceName } if ($Stop.IsPresent) { - Stop-PodeService -Name $ServiceName - exit + return Stop-PodeService -Name $ServiceName } if ($Suspend.IsPresent) { - Suspend-PodeService -Name $ServiceName - exit + return Suspend-PodeService -Name $ServiceName } if ($Resume.IsPresent) { - Resume-PodeService -Name $ServiceName - exit + return Resume-PodeService -Name $ServiceName } if ($Query.IsPresent) { - Get-PodeService -Name $ServiceName - exit + return Get-PodeService -Name $ServiceName } if ($Restart.IsPresent) { - Restart-PodeService -Name $ServiceName - exit + return Restart-PodeService -Name $ServiceName } # Start the Pode server diff --git a/src/Pode.psm1 b/src/Pode.psm1 index 3e2d95553..53622260f 100644 --- a/src/Pode.psm1 +++ b/src/Pode.psm1 @@ -135,6 +135,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/Private/Service.ps1 b/src/Private/Service.ps1 index 195b14fd6..0f3e76299 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -312,14 +312,14 @@ function Register-PodeMacService { } else { $plistPath = "/Library/LaunchDaemons/$($nameService).plist" - sudo cp $tempFile $plistPath + & sudo cp $tempFile $plistPath #set rw r r permissions - sudo chmod 644 $plistPath + & sudo chmod 644 $plistPath - sudo chown root:wheel $plistPath + & sudo chown root:wheel $plistPath # Load the plist with launchctl - sudo launchctl load $plistPath + & sudo launchctl load $plistPath } @@ -455,7 +455,7 @@ WantedBy=multi-user.target Write-Verbose -Message "Service '$nameService' ExecStart : $execStart)." - sudo cp $tempFile "/etc/systemd/system/$nameService" + & sudo cp $tempFile "/etc/systemd/system/$nameService" Remove-Item -path $tempFile -ErrorAction SilentlyContinue @@ -652,7 +652,7 @@ function Confirm-PodeAdminPrivilege { } # 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 + Write-PodeHost "Insufficient privileges. This script must be run as root or with 'sudo' permissions to continue." -ForegroundColor Red exit } } @@ -745,7 +745,7 @@ function Disable-PodeLinuxService { $Name ) $nameService = Get-PodeRealServiceName -Name $Name - $systemctlDisable = sudo systemctl disable $nameService 2>&1 + $systemctlDisable = & sudo systemctl disable $nameService 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($systemctlDisable -join '`n') return $success @@ -775,7 +775,7 @@ function Enable-PodeLinuxService { [string] $Name ) - $systemctlEnable = sudo systemctl enable $Name 2>&1 + $systemctlEnable = & sudo systemctl enable $Name 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($systemctlEnable -join '`n') return $success @@ -807,7 +807,7 @@ function Stop-PodeLinuxService { ) $nameService = Get-PodeRealServiceName -Name $Name #return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) - $serviceStopInfo = sudo systemctl stop $nameService 2>&1 + $serviceStopInfo = & sudo systemctl stop $nameService 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($serviceStopInfo -join "`n") return $success @@ -838,7 +838,7 @@ function Start-PodeLinuxService { $Name ) $nameService = Get-PodeRealServiceName -Name $Name - $serviceStartInfo = sudo systemctl start $nameService 2>&1 + $serviceStartInfo = & sudo systemctl start $nameService 2>&1 $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($serviceStartInfo -join "`n") return $success @@ -879,10 +879,10 @@ function Test-PodeMacOsServiceIsRegistered { $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) } if ($sudo) { - $systemctlStatus = sudo launchctl list $nameService 2>&1 + $systemctlStatus = & sudo launchctl list $nameService 2>&1 } else { - $systemctlStatus = launchctl list $nameService 2>&1 + $systemctlStatus = & launchctl list $nameService 2>&1 } $isRegistered = ($LASTEXITCODE -eq 0) Write-Verbose -Message ($systemctlStatus -join '`n') @@ -998,10 +998,10 @@ function Test-PodeMacOsServiceIsActive { $nameService = Get-PodeRealServiceName -Name $Name $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) if ($sudo) { - $serviceInfo = sudo launchctl list $nameService + $serviceInfo = & sudo launchctl list $nameService } else { - $serviceInfo = launchctl list $nameService + $serviceInfo = & launchctl list $nameService } $isActive = $serviceInfo -match '"PID" = (\d+);' Write-Verbose -Message ($serviceInfo -join "`n") @@ -1035,10 +1035,10 @@ function Get-PodeMacOsServicePid { $nameService = Get-PodeRealServiceName -Name $Name $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) if ($sudo) { - $serviceInfo = sudo launchctl list $nameService + $serviceInfo = & sudo launchctl list $nameService } else { - $serviceInfo = launchctl list $nameService + $serviceInfo = & launchctl list $nameService } $pidString = $serviceInfo -match '"PID" = (\d+);' Write-Verbose -Message ($serviceInfo -join "`n") @@ -1073,10 +1073,10 @@ function Disable-PodeMacOsService { $nameService = Get-PodeRealServiceName -Name $Name $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) if ($sudo) { - $systemctlDisable = sudo launchctl unload "/Library/LaunchDaemons/$nameService.plist" 2>&1 + $systemctlDisable = & sudo launchctl unload "/Library/LaunchDaemons/$nameService.plist" 2>&1 } else { - $systemctlDisable = launchctl unload "$HOME/Library/LaunchAgents/$nameService.plist" 2>&1 + $systemctlDisable = & launchctl unload "$HOME/Library/LaunchAgents/$nameService.plist" 2>&1 } $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($systemctlDisable -join '`n') @@ -1140,10 +1140,10 @@ function Start-PodeMacOsService { $nameService = Get-PodeRealServiceName -Name $Name $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) if ($sudo) { - $serviceStartInfo = sudo launchctl start $nameService 2>&1 + $serviceStartInfo = & sudo launchctl start $nameService 2>&1 } else { - $serviceStartInfo = launchctl start $nameService 2>&1 + $serviceStartInfo = & launchctl start $nameService 2>&1 } $success = $LASTEXITCODE -eq 0 Write-Verbose -Message ($serviceStartInfo -join "`n") @@ -1227,10 +1227,10 @@ function Send-PodeServiceSignal { # Send the signal based on the privilege level if ($svc.Sudo) { - sudo /bin/kill -$($level) $svc.Pid + & sudo /bin/kill -$($level) $svc.Pid } else { - /bin/kill -$($level) $svc.Pid + & /bin/kill -$($level) $svc.Pid } # Check the exit code to determine if the signal was sent successfully @@ -1360,6 +1360,7 @@ function Wait-PodeServiceStatus { - Sudo: A boolean indicating whether elevated privileges are required. .NOTES + - Possible states: Running,Stopped,Suspended,Starting,Stopping,Pausing,Resuming,Unknown - Requires administrative/root privileges to access service information on Linux and macOS. - Platform-specific behaviors: - **Windows**: Retrieves service information via the `Win32_Service` class. @@ -1394,12 +1395,15 @@ function Get-PodeServiceStatus { 'ContinuePending' { $status = 'Resuming' } default { $status = 'Unknown' } } - return @{ - Name = $Name - Status = $status - Pid = $service.ProcessId - Sudo = $true + return [PSCustomObject]@{ + PsTypeName = 'PodeService' + Name = $Name + Status = $status + Pid = $service.ProcessId + Sudo = $true + PathName = $service.PathName } + } else { Write-Verbose -Message "Service '$Name' not found." @@ -1449,11 +1453,13 @@ function Get-PodeServiceStatus { $status = 'Stopped' } } - return @{ - Name = $Name - Status = $status - Pid = $servicePid - Sudo = $true + return [PSCustomObject]@{ + PsTypeName = 'PodeService' + Name = $Name + Status = $status + Pid = $servicePid + Sudo = $true + PathName = "/etc/systemd/system/$nameService" } } else { @@ -1476,32 +1482,29 @@ function Get-PodeServiceStatus { $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) - # Check if the service has a PID entry - if ($servicePid -ne 0) { - if ($sudo) { - $stateFilePath = "/Library/LaunchDaemons/PodeMonitor/$servicePid.state" - } - else { - $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" - } - if (Test-Path -Path $stateFilePath) { - $status = Get-Content -Path $stateFilePath -Raw - $status = $status.Substring(0, 1).ToUpper() + $status.Substring(1) - } - return @{ - Name = $Name - Status = $status - Pid = $servicePid - Sudo = $sudo - } + if ($sudo) { + $stateFilePath = "/Library/LaunchDaemons/PodeMonitor/$servicePid.state" + $plistPath="/Library/LaunchDaemons/$($nameService).plist" } else { - return @{ - Name = $Name - Status = 'Stopped' - Pid = 0 - Sudo = $sudo - } + $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" + $plistPath="$($HOME)/Library/LaunchAgents/$($nameService).plist" + } + 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 = $true + PathName = $plistPath } } else { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index a5ab332b5..886819399 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -307,82 +307,80 @@ function Register-PodeService { - 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, + [string] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'Async')] - [switch] - $Async, + [switch] $Async, [Parameter(Mandatory = $false, ParameterSetName = 'Async')] - [int] - $Timeout = 10 + [ValidateRange(1, 300)] + [int] $Timeout = 10 ) - # 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 { + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + # Get the service status $service = Get-PodeServiceStatus -Name $Name if (!$service) { - # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } - if ($service.Status -eq 'Running') { - Write-Verbose -Message "Service '$Name' is already Running." - return $true - } - if ($service.Status -ne 'Stopped') { - Write-Verbose -Message "Service '$Name' is not Stopped." - return $false - } - if (Test-PodeIsWindows) { - if ( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "start '$Name'") { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout - } - } + Write-Verbose -Message "Service '$Name' current state: $($service.Status)." - throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe start {0}', $Name) + # 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) { - # Start the service - if ((Start-PodeLinuxService -Name $Name)) { - 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) - + $serviceStarted = Start-PodeLinuxService -Name $Name } elseif ($IsMacOS) { - # Start the service - if ((Start-PodeMacOsService -Name $Name)) { - 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) + $serviceStarted = Start-PodeMacOsService -Name $Name + } + + # 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 { @@ -390,7 +388,6 @@ function Start-PodeService { Write-Error -Exception $_.Exception return $false } - return $true } <# @@ -448,76 +445,75 @@ function Stop-PodeService { $Async, [Parameter(Mandatory = $false, ParameterSetName = 'Async')] + [ValidateRange(1, 300)] [int] $Timeout = 10 ) try { - # Ensure the script is running with the necessary administrative/root privileges. - # Exits the script if the current user lacks the required privileges. + # Ensure administrative/root privileges Confirm-PodeAdminPrivilege - if (Test-PodeIsWindows) { + # Get the service status + $service = Get-PodeServiceStatus -Name $Name + if (!$service) { + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($service) { - # Check if the service is running - if ($service.Status -eq 'Running' -or $service.Status -eq 'Paused') { - if ( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "stop '$Name'") { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Stopped -Timeout $Timeout - } - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe stop', $Name) - } - else { - Write-Verbose -Message "Service '$Name' is not running." - } + 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 } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + { $_ -eq 'Running' -or $_ -eq 'Suspended' } { + Write-Verbose -Message "Service '$Name' is currently $($service.Status). Attempting to stop..." } - } - elseif ($IsLinux) { - #Stop the service - if (( Stop-PodeLinuxService -Name $Name)) { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Stopped -Timeout $Timeout - } + { $_ -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 } + } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl stop', $Name) + # 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) { - if ((Stop-PodeMacOsService $Name)) { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Stopped -Timeout $Timeout - } - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) + $serviceStopped = Stop-PodeMacOsService -Name $Name } + 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 } - return $true } + <# .SYNOPSIS Suspend a specified service on Windows systems. @@ -555,74 +551,72 @@ function Suspend-PodeService { [OutputType([bool])] param( [Parameter(Mandatory = $true)] - [string] - $Name, + [string] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'Async')] - [switch] - $Async, + [switch] $Async, [Parameter(Mandatory = $false, ParameterSetName = 'Async')] - [int] - $Timeout = 10 + [ValidateRange(1, 300)] + [int] $Timeout = 10 ) - # 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 { + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + # Get the service status $service = Get-PodeServiceStatus -Name $Name if (!$service) { - # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } - if ($service.Status -eq 'Suspended') { - Write-Verbose -Message "Service '$Name' is already suspended." - return $true - } - if ($service.Status -ne 'Running') { - Write-Verbose -Message "Service '$Name' is not running." - return $false - } - if (Test-PodeIsWindows) { - if (( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "pause '$Name'")) { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Suspended -Timeout $Timeout - } + + 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 + } + } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe pause', $Name) + # Suspend the service based on the OS + $serviceSuspended = $false + if (Test-PodeIsWindows) { + $serviceSuspended = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "pause '$Name'" + } + elseif ($IsLinux -or $IsMacOS) { + $serviceSuspended = ( Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP') } - elseif ($IsLinux) { - if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP')) { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Suspended -Timeout $Timeout - } - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f ' sudo /bin/kill -SIGTSTP', $Name) + # Check if the service suspend command failed + if (!$serviceSuspended) { + throw ($PodeLocale.serviceCommandFailedException -f 'Suspend', $Name) } - elseif ($IsMacOS) { - if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGTSTP')) { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Suspended -Timeout $Timeout - } - } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f '/bin/kill -SIGTSTP ', $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 { @@ -630,9 +624,9 @@ function Suspend-PodeService { Write-Error -Exception $_.Exception return $false } - return $true } + <# .SYNOPSIS Resume a specified service on Windows systems. @@ -665,85 +659,84 @@ function Suspend-PodeService { - If the service is not registered, an error is thrown. #> - function Resume-PodeService { [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([bool])] param( [Parameter(Mandatory = $true)] - [string] - $Name, + [string] $Name, [Parameter(Mandatory = $true, ParameterSetName = 'Async')] - [switch] - $Async, + [switch] $Async, [Parameter(Mandatory = $false, ParameterSetName = 'Async')] - [int] - $Timeout = 10 + [ValidateRange(1, 300)] + [int] $Timeout = 10 ) - # 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 { + # Ensure administrative/root privileges + Confirm-PodeAdminPrivilege + # Get the service status $service = Get-PodeServiceStatus -Name $Name if (!$service) { - # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } - if ($service.Status -ne 'Suspended') { - Write-Verbose -Message "Service '$Name' is not Suspended." - return $false - } - if (Test-PodeIsWindows) { - if (( Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "continue '$Name'")) { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout - } + 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 } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sc.exe continue', $Name) - } - elseif ($IsLinux) { - if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT')) { - if ($Async) { - return $true - } - else { - return Wait-PodeServiceStatus -Name $Name -Status Running -Timeout $Timeout - } + '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 + } + } - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f ' sudo /bin/kill -SIGCONT', $Name) + # Resume the service based on the OS + $serviceResumed = $false + if (Test-PodeIsWindows) { + $serviceResumed = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "continue '$Name'" + } + elseif ($IsLinux -or $IsMacOS) { + $serviceResumed = Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT' } - elseif ($IsMacOS) { - if (( Send-PodeServiceSignal -Name $Name -Signal 'SIGCONT')) { - 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 '/bin/kill -SIGCONT ', $Name) + # 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 } - return $true } <# @@ -780,189 +773,125 @@ function Resume-PodeService { #> function Unregister-PodeService { param( - [Parameter()] - [switch]$Force, - [Parameter(Mandatory = $true)] [string] - $Name + $Name, + + [Parameter()] + [switch] + $Force ) - # Ensure the script is running with the necessary administrative/root privileges. - # Exits the script if the current user lacks the required privileges. + + # Ensure administrative/root privileges Confirm-PodeAdminPrivilege - if (Test-PodeIsWindows) { - # Check if the service exists - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if (-not $service) { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f "$Name") - } + # Get the service status + $service = Get-PodeServiceStatus -Name $Name + if (!$service) { + throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) + } - try { - $pathName = $service.BinaryPathName - # Check if the service is running before attempting to stop it - if ($service.Status -eq 'Running') { - if ($Force.IsPresent) { - $null = Invoke-PodeWinElevatedCommand -Command 'sc' -Arguments "stop '$Name'" - if (!( Stop-PodeService -Name $Name)) { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'Stop-Service', $Name) - } - } - else { - # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f $Name ) - } - } + Write-Verbose -Message "Service '$Name' current state: $($service.Status)." - # Remove the service - $null = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "delete '$Name'" - $service = Get-Service -Name $Name -ErrorAction SilentlyContinue - if ($null -ne $service) { - Write-Verbose -Message "Service '$Name' unregistered failed." - throw ($PodeLocale.serviceUnRegistrationException -f $Name) - } - Write-Verbose -Message "Service '$Name' unregistered successfully." + # 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." + Stop-PodeService -Name $Name + 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 + } + } - # Remove the service configuration - if ($pathName) { - $binaryPath = $pathName.trim('"').split('" "') - if ((Test-Path -Path ($binaryPath[1]) -PathType Leaf)) { - Remove-Item -Path ($binaryPath[1]) -ErrorAction Break - } - } - return $true + 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) } - catch { - $_ | Write-PodeErrorLog - Write-Error -Exception $_.Exception - return $false + + Write-Verbose -Message "Service '$Name' unregistered successfully." + + # Remove the service configuration + if ($service.PathName) { + $binaryPath = $service.PathName.trim('"').split('" "') + if ($binaryPath.Count -gt 1 -and (Test-Path -Path ($binaryPath[1]) -PathType Leaf)) { + Remove-Item -Path ($binaryPath[1]) -ErrorAction Break + } } + return $true + } elseif ($IsLinux) { - try { - # Check if the service is already registered - if ((Test-PodeLinuxServiceIsRegistered $Name)) { - # Check if the service is active - if ((Test-PodeLinuxServiceIsActive -Name $Name)) { - if ($Force.IsPresent) { - #Stop the service - if (!( Stop-PodeService -Name $Name)) { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'sudo systemctl stop', $Name) - } - } - else { - # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f $Name) - } - } - if ((Disable-PodeLinuxService -Name $Name)) { - # Read the content of the service file - $serviceFilePath = "/etc/systemd/system/$(Get-PodeRealServiceName -Name $Name)" - if ((Test-path -path $serviceFilePath -PathType Leaf)) { - $serviceFileContent = sudo cat $serviceFilePath - - # 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) - if ((Test-Path -Path $settingsFile -PathType Leaf)) { - Remove-Item -Path $settingsFile - } - sudo rm $serviceFilePath - - Write-Verbose -Message "Service '$Name' unregistered successfully." - } - sudo systemctl daemon-reload - } - else { - Write-Verbose -Message "Service '$Name' unregistered failed." - throw ($PodeLocale.serviceUnRegistrationException -f $Name) - } - } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $Name ) - } - return $true + $null = Disable-PodeLinuxService -Name $Name + if (Get-PodeService -Name $Name -ErrorAction SilentlyContinue) { + Write-Verbose -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) } - catch { - $_ | Write-PodeErrorLog - Write-Error -Exception $_.Exception - return $false + + 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) + + & 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) { - try { - # Check if the service is already registered - if (Test-PodeMacOsServiceIsRegistered $Name) { - # Check if the service is active - if ((Test-PodeMacOsServiceIsActive -Name $Name)) { - if ($Force.IsPresent) { - #Stop the service - if (!( Stop-PodeService -Name $Name)) { - # Service command '{0}' failed on service '{1}'. - throw ($PodeLocale.serviceCommandFailedException -f 'launchctl stop', $Name) - } - } - else { - # Service is running. Use the -Force parameter to forcefully stop." - throw ($Podelocale.serviceIsRunningException -f $Name) - } - } + # Disable and unregister the service + Disable-PodeMacOsService -Name $Name + if (Get-PodeService -Name $Name -ErrorAction SilentlyContinue) { + Write-Verbose -Message "Service '$Name' unregistered failed." + throw ($PodeLocale.serviceUnRegistrationException -f $Name) + } - if ((Disable-PodeMacOsService -Name $Name)) { - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$(Get-PodeRealServiceName -Name $Name).plist" -PathType Leaf) - if ($sudo) { - $plistFilePath = "/Library/LaunchDaemons/$(Get-PodeRealServiceName -Name $Name).plist" - } - else { - $plistFilePath = "$($HOME)/Library/LaunchAgents/$(Get-PodeRealServiceName -Name $Name).plist" - } - #Check if the plist file exists - if (Test-Path -Path $plistFilePath) { - # Read the content of the plist file - $plistXml = [xml](Get-Content -Path $plistFilePath -Raw) - - # Extract the second string in the ProgramArguments array (the settings file path) - $settingsFile = $plistXml.plist.dict.array.string[1] - if ($sudo) { - sudo rm $settingsFile - sudo rm $plistFilePath - } - else { - if ((Test-Path -Path $settingsFile -PathType Leaf)) { - Remove-Item -Path $settingsFile - } - - Remove-Item -Path $plistFilePath -ErrorAction Break - } - - Write-Verbose -Message "Service '$Name' unregistered successfully." - } + 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 { - Write-Verbose -Message "Service '$Name' unregistered failed." - throw ($PodeLocale.serviceUnRegistrationException -f $Name) + 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." } } - else { - # Service is not registered - throw ($PodeLocale.serviceIsNotRegisteredException -f $Name ) - } - return $true - } - catch { - $_ | Write-PodeErrorLog - Write-Error -Exception $_.Exception - return $false } } } @@ -1147,3 +1076,5 @@ function Restart-PodeService { Write-Verbose -Message "Service '$Name' restart operation completed successfully." return $true } + + diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 63693366e..7c928d413 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -7,9 +7,10 @@ param() Describe 'Service Lifecycle' { it 'register' { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register + $success | Should -BeTrue Start-Sleep 10 - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query if ($IsMacOS) { $status.Status | Should -Be 'Running' $status.Pid | Should -BeGreaterThan 0 @@ -25,10 +26,11 @@ Describe 'Service Lifecycle' { it 'start' -Skip:( $IsMacOS) { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start + $success | Should -BeTrue Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -BeGreaterThan 0 @@ -36,10 +38,11 @@ Describe 'Service Lifecycle' { } it 'pause' { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend + $success | Should -BeTrue Start-Sleep 2 # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Suspended' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -BeGreaterThan 0 @@ -47,19 +50,21 @@ Describe 'Service Lifecycle' { } it 'resume' { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -resume + $success | Should -BeTrue Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -BeGreaterThan 0 $webRequest.Content | Should -Be 'Hello, Service!' } it 'stop' { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop + $success | Should -BeTrue Start-Sleep 2 - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Stopped' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -Be 0 @@ -67,11 +72,12 @@ Describe 'Service Lifecycle' { { Invoke-WebRequest -uri http://localhost:8080 } | Should -Throw } - it 're-start' { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start + it 're-start' { + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Start + $success | Should -BeTrue Start-Sleep 2 $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Running' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -BeGreaterThan 0 @@ -80,11 +86,11 @@ Describe 'Service Lifecycle' { it 're-stop' { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop + $success | Should -BeTrue Start-Sleep 2 - - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status.Status | Should -Be 'Stopped' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -Be 0 @@ -93,9 +99,10 @@ Describe 'Service Lifecycle' { } it 'unregister' { - . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Unregister + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Unregister + $success | Should -BeTrue Start-Sleep 2 - $status = . "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query $status | Should -BeNullOrEmpty } From d6a026f4aafbec4ef597331bf3beb2705958a843 Mon Sep 17 00:00:00 2001 From: MDaneri Date: Wed, 27 Nov 2024 10:25:18 -0800 Subject: [PATCH 74/93] fix linux --- src/Public/Service.ps1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 886819399..fb32893a8 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -829,8 +829,8 @@ function Unregister-PodeService { } elseif ($IsLinux) { - $null = Disable-PodeLinuxService -Name $Name - if (Get-PodeService -Name $Name -ErrorAction SilentlyContinue) { + if (! (Disable-PodeLinuxService -Name $Name)) { + # if (Get-PodeService -Name $Name -ErrorAction SilentlyContinue) { Write-Verbose -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } @@ -839,23 +839,23 @@ function Unregister-PodeService { # Read the content of the service file if ((Test-path -path $service.PathName -PathType Leaf)) { - $serviceFileContent = & sudo cat $service.PathName + $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) + $settingsFile = $execStart.Substring($index).trim('"') - & sudo rm $settingsFile + & sudo rm $settingsFile Write-Verbose -Message "Settings file '$settingsFile' removed." - & sudo rm $service.PathName + & sudo rm $service.PathName Write-Verbose -Message "Service file '$($service.PathName)' removed." } # Reload systemd to apply changes - & sudo systemctl daemon-reload + & sudo systemctl daemon-reload Write-Verbose -Message 'Systemd daemon reloaded.' return $true } @@ -878,10 +878,10 @@ function Unregister-PodeService { # 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 + & sudo rm $settingsFile Write-Verbose -Message "Settings file '$settingsFile' removed." - & sudo rm $service.PathName + & sudo rm $service.PathName Write-Verbose -Message "Service file '$($service.PathName)' removed." } else { From 40d5bbc403fc9f2d04ebc366354340ed5ac3c9a8 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 27 Nov 2024 14:30:29 -0800 Subject: [PATCH 75/93] fixes for Powershell 5.1 --- pode.build.ps1 | 2 +- src/PodeMonitor/PodeMonitor.csproj | 2 +- src/Private/Helpers.ps1 | 48 ++++++++++++++++++++++++++++++ src/Public/Service.ps1 | 34 +++++++++++++-------- 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index eca54c59f..ab4b59439 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -574,7 +574,7 @@ function Invoke-PodeBuildDotnetMonitorSrvBuild() { $AssemblyVersion = '' } - foreach ($target in @('win-x64', 'win-arm64' , 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64')) { + foreach ($target in @('win-x64', 'win-arm64' , 'linux-x64', 'linux-arm64', 'osx-x64', 'osx-arm64','linux-arm','win-x86','linux-musl-x64')) { $DefineConstants = @() $ParamConstants = '' diff --git a/src/PodeMonitor/PodeMonitor.csproj b/src/PodeMonitor/PodeMonitor.csproj index 138614320..cba19cd46 100644 --- a/src/PodeMonitor/PodeMonitor.csproj +++ b/src/PodeMonitor/PodeMonitor.csproj @@ -8,7 +8,7 @@ true true false - win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64 + win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64;linux-arm;win-x86;linux-musl-x64 diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 8b33314ff..47f3bf846 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -4040,3 +4040,51 @@ function Convert-PodeMillisecondsToReadable { return $parts -join ':' } + + +<# +.SYNOPSIS + Determines the OS architecture for the current system. + +.DESCRIPTION + This function detects the operating system's architecture and converts it into a format + compatible with PowerShell installation requirements. It handles both Windows and Unix-based + systems and maps various architecture identifiers to PowerShell-supported names (e.g., 'x64', 'arm64'). + +.OUTPUTS + [string] - The architecture string, such as 'x64', 'x86', 'arm64', or 'arm32'. + +.EXAMPLE + $arch = Get-PodeOSPwshArchitecture + Write-Host "Current architecture: $arch" + +.NOTES + - For Windows, the architecture is derived from the `PROCESSOR_ARCHITECTURE` environment variable. + - For Unix-based systems, the architecture is determined using the `uname -m` command. + - If the architecture is not supported, the function throws an exception. +#> +function Get-PodeOSPwshArchitecture { + # Initialize architecture variable + $arch = [string]::Empty + + # Detect architecture on Unix-based systems (Linux/macOS) + if ($IsLinux -or $IsMacOS) { + $arch = uname -m + }else{ + # Architecture on Windows + $arch = $env:PROCESSOR_ARCHITECTURE + } + + # Convert detected architecture to a PowerShell-compatible format + switch ($arch.ToLowerInvariant()) { + 'amd64' { return 'x64' } # 64-bit architecture (AMD64) + 'x86' { return 'x86' } # 32-bit architecture + 'x86_64' { return 'x64' } # 64-bit architecture (x86_64) + 'armv7*' { return 'arm32' } # 32-bit ARM architecture + 'aarch64*' { return 'arm64' } # 64-bit ARM architecture + 'arm64' { return 'arm64' } # Explicit ARM64 + 'arm64*' { return 'arm64' } # Pattern matching for ARM64 + 'armv8*' { return 'arm64' } # ARM v8 series + default { throw "Unsupported architecture: $($arch)" } # Throw exception for unsupported architectures + } +} \ No newline at end of file diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index fb32893a8..ffe47e970 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -160,6 +160,7 @@ function Register-PodeService { 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)) { @@ -205,7 +206,7 @@ function Register-PodeService { $jsonContent | ConvertTo-Json | Set-Content -Path $settingsFile -Encoding UTF8 # Determine OS architecture and call platform-specific registration functions - $osArchitecture = ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture).ToString().ToLower() + $osArchitecture = Get-PodeOSPwshArchitecture # 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' @@ -222,9 +223,20 @@ function Register-PodeService { SecurityDescriptorSddl = $SecurityDescriptorSddl OsArchitecture = "win-$osArchitecture" } - $operation = Register-PodeMonitorWindowsService @param + $operation = Register-PodeMonitorWindowsService @param } elseif ($IsLinux) { + $musl = '' + if ($osArchitecture -eq 'x64') { + # Check for musl libc + if (Get-Command ldd -ErrorAction SilentlyContinue) { + $lddOutput = ldd --version 2>&1 + if ($lddOutput -match 'musl') { + $musl = 'musl-' + } + } + } + $param = @{ Name = $Name Description = $Description @@ -233,7 +245,7 @@ function Register-PodeService { User = $User Group = $Group Start = $Start - OsArchitecture = "linux-$osArchitecture" + OsArchitecture = "linux-$($musl)$($osArchitecture)" } $operation = Register-PodeLinuxService @param } @@ -818,10 +830,10 @@ function Unregister-PodeService { Write-Verbose -Message "Service '$Name' unregistered successfully." # Remove the service configuration - if ($service.PathName) { - $binaryPath = $service.PathName.trim('"').split('" "') - if ($binaryPath.Count -gt 1 -and (Test-Path -Path ($binaryPath[1]) -PathType Leaf)) { - Remove-Item -Path ($binaryPath[1]) -ErrorAction Break + if ($service.PathName -match '"([^"]+)" "([^"]+)"') { + $argument = $Matches[2] + if ( (Test-Path -Path ($argument) -PathType Leaf)) { + Remove-Item -Path ($argument) -ErrorAction SilentlyContinue } } return $true @@ -830,7 +842,6 @@ function Unregister-PodeService { elseif ($IsLinux) { if (! (Disable-PodeLinuxService -Name $Name)) { - # if (Get-PodeService -Name $Name -ErrorAction SilentlyContinue) { Write-Verbose -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } @@ -862,8 +873,7 @@ function Unregister-PodeService { elseif ($IsMacOS) { # Disable and unregister the service - Disable-PodeMacOsService -Name $Name - if (Get-PodeService -Name $Name -ErrorAction SilentlyContinue) { + if (!(Disable-PodeMacOsService -Name $Name)) { Write-Verbose -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } @@ -1020,8 +1030,8 @@ function Restart-PodeService { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } - if (@('Running', 'Suspended' ) -inotcontains $service.Status ) { - Write-Verbose -Message "Service '$Name' is not Running or Suspended." + if ('Running' -ne $service.Status ) { + Write-Verbose -Message "Service '$Name' is not Running." return $false } if (Test-PodeIsWindows) { From 1761077e5071899e8bbe5690f5fc65dc5b14e428 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 27 Nov 2024 15:46:16 -0800 Subject: [PATCH 76/93] Added log rotation and additional features --- src/PodeMonitor/PodeLogLevel.cs | 15 +++ src/PodeMonitor/PodeMonitor.cs | 62 ++++++------- src/PodeMonitor/PodeMonitorLogger.cs | 96 +++++++++++++++----- src/PodeMonitor/PodeMonitorMain.cs | 42 +++++++-- src/PodeMonitor/PodeMonitorWindowsService.cs | 22 ++--- src/PodeMonitor/PodeMonitorWorker.cs | 44 ++++----- src/PodeMonitor/PodeMonitorWorkerOptions.cs | 20 +++- src/Private/Helpers.ps1 | 69 +++++++++----- src/Private/Service.ps1 | 8 +- src/Public/Service.ps1 | 71 ++++++++++----- 10 files changed, 301 insertions(+), 148 deletions(-) create mode 100644 src/PodeMonitor/PodeLogLevel.cs 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 index 4176746f4..18d30fbe3 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -53,8 +53,6 @@ public class PodeMonitor public bool DisableTermination { get => _disableTermination; } - - /// /// Initializes a new instance of the class with the specified configuration options. /// @@ -73,7 +71,7 @@ public PodeMonitor(PodeMonitorWorkerOptions options) // Generate a unique pipe name _pipeName = PipeNameGenerator.GeneratePipeName(); - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName}"); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName}"); // Define the state file path only for Linux/macOS if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { @@ -93,7 +91,7 @@ public PodeMonitor(PodeMonitorWorkerOptions options) } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to create state directory at {stateDirectory}: {ex.Message}"); throw; } @@ -102,7 +100,7 @@ public PodeMonitor(PodeMonitorWorkerOptions options) _stateFilePath = Path.Combine(stateDirectory, $"{Environment.ProcessId}.state"); - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName} and state file: {_stateFilePath}"); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName} and state file: {_stateFilePath}"); } } @@ -117,7 +115,7 @@ public void StartPowerShellProcess() { if ((DateTime.Now - _lastLogTime).TotalMinutes >= 5) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is Alive."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is Alive."); _lastLogTime = DateTime.Now; } return; @@ -144,7 +142,7 @@ public void StartPowerShellProcess() { if (!string.IsNullOrEmpty(args.Data)) { - PodeMonitorLogger.Log(LogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "Pode", _powerShellProcess.Id, args.Data); ParseServiceState(args.Data); } }; @@ -152,7 +150,7 @@ public void StartPowerShellProcess() // Subscribe to the error stream for logging errors _powerShellProcess.ErrorDataReceived += (sender, args) => { - PodeMonitorLogger.Log(LogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "Pode", _powerShellProcess.Id, args.Data); }; // Start the process and begin reading the output/error streams @@ -161,12 +159,12 @@ public void StartPowerShellProcess() _powerShellProcess.BeginErrorReadLine(); _lastLogTime = DateTime.Now; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process started successfully."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process started successfully."); } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to start Pode process: {ex.Message}"); - PodeMonitorLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to start Pode process: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); } } } @@ -180,7 +178,7 @@ public void StopPowerShellProcess() { if (_powerShellProcess == null || (_powerShellProcess.HasExited && Process.GetProcessById(_powerShellProcess.Id) == null)) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is not running."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process is not running."); return; } @@ -189,24 +187,24 @@ public void StopPowerShellProcess() if (InitializePipeClientWithRetry()) { SendPipeMessage("shutdown"); - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); - if (_powerShellProcess != null && !_powerShellProcess.HasExited ) + if (_powerShellProcess != null && !_powerShellProcess.HasExited) { - PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Pode process has exited:{_powerShellProcess.HasExited} Id:{_powerShellProcess.Id}"); + PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Pode process has exited:{_powerShellProcess.HasExited} Id:{_powerShellProcess.Id}"); - PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully. Killing process."); + PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Pode process did not terminate gracefully. Killing process."); _powerShellProcess.Kill(); } - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process stopped successfully."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pode process stopped successfully."); } } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error stopping Pode process: {ex.Message}"); - PodeMonitorLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error stopping Pode process: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); } finally { @@ -245,13 +243,13 @@ private void ExecutePipeCommand(string command) if (InitializePipeClientWithRetry()) { SendPipeMessage(command); - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"{command.ToUpper()} command sent to Pode process."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"{command.ToUpper()} command sent to Pode process."); } } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error executing {command} command: {ex.Message}"); - PodeMonitorLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error executing {command} command: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); } finally { @@ -287,7 +285,7 @@ private void ParseServiceState(string output) UpdateServiceState(ServiceState.Stopping); break; default: - PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Unknown service state: {state}"); + PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Unknown service state: {state}"); UpdateServiceState(ServiceState.Unknown); break; } @@ -301,7 +299,7 @@ private void ParseServiceState(string output) private void UpdateServiceState(ServiceState state) { _state = state; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state updated to: {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()) { @@ -340,7 +338,7 @@ private bool InitializePipeClientWithRetry(int maxRetries = 3) if (!_pipeClient.IsConnected) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Connecting to pipe server (Attempt {attempts + 1})..."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Connecting to pipe server (Attempt {attempts + 1})..."); _pipeClient.Connect(10000); // Timeout of 10 seconds } @@ -348,7 +346,7 @@ private bool InitializePipeClientWithRetry(int maxRetries = 3) } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Pipe connection attempt {attempts + 1} failed: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Pipe connection attempt {attempts + 1} failed: {ex.Message}"); } attempts++; @@ -371,8 +369,8 @@ private void SendPipeMessage(string message) } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error sending message to pipe: {ex.Message}"); - PodeMonitorLogger.Log(LogLevel.DEBUG, ex); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Error sending message to pipe: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, ex); } } @@ -402,11 +400,11 @@ private void WriteServiceStateToFile(ServiceState state) try { File.WriteAllText(_stateFilePath, state.ToString().ToLowerInvariant()); - PodeMonitorLogger.Log(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Service state written to file: {_stateFilePath}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Service state written to file: {_stateFilePath}"); } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to write service state to file: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to write service state to file: {ex.Message}"); } } } @@ -423,12 +421,12 @@ private void DeleteServiceStateFile() if (File.Exists(_stateFilePath)) { File.Delete(_stateFilePath); - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state file deleted: {_stateFilePath}"); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state file deleted: {_stateFilePath}"); } } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to delete service state file: {ex.Message}"); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, $"Failed to delete service state file: {ex.Message}"); } } } diff --git a/src/PodeMonitor/PodeMonitorLogger.cs b/src/PodeMonitor/PodeMonitorLogger.cs index 8ccb10f64..c2be7bab3 100644 --- a/src/PodeMonitor/PodeMonitorLogger.cs +++ b/src/PodeMonitor/PodeMonitorLogger.cs @@ -4,36 +4,35 @@ namespace PodeMonitor { - public enum LogLevel - { - 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 - } + /// + /// 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 LogLevel _minLogLevel = LogLevel.INFO; // Default minimum log level + 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. - public static void Initialize(string filePath, LogLevel level) + /// Maximum log file size in bytes before rotation. + public static void Initialize(string filePath, PodeLogLevel level, long maxFileSizeInBytes = DefaultMaxFileSize) { try { - // Update the log file path and minimum log level + // Set the log file path and validate it if (!string.IsNullOrWhiteSpace(filePath)) { + ValidateLogPath(filePath); _logFilePath = filePath; } @@ -45,9 +44,12 @@ public static void Initialize(string filePath, LogLevel level) using (File.Create(_logFilePath)) { }; } + // Perform log rotation if necessary + RotateLogFile(maxFileSizeInBytes); + // Log initialization success - Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, - "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}", _logFilePath, _minLogLevel); + Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, + "Logger initialized. LogFilePath: {0}, MinLogLevel: {1}, MaxFileSize: {2} bytes", _logFilePath, _minLogLevel, maxFileSizeInBytes); } catch (Exception ex) { @@ -56,18 +58,18 @@ public static void Initialize(string filePath, LogLevel level) } /// - /// Logs a message to the log file with the specified log level and context. + /// 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(LogLevel level, string context, int pid, string message = "", params object[] args) + 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 log level or empty messages + return; // Skip logging for levels below the minimum or empty messages } try @@ -99,12 +101,13 @@ public static void Log(LogLevel level, string context, int pid, string 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(LogLevel level, Exception exception, string message = null, params object[] args) + public static void Log(PodeLogLevel level, Exception exception, string message = null, params object[] args) { if (level < _minLogLevel || (exception == null && string.IsNullOrEmpty(message))) { @@ -118,6 +121,7 @@ public static void Log(LogLevel level, Exception exception, string message = nul // Format the message if provided string logMessage = string.Empty; + if (!string.IsNullOrEmpty(message)) { // Sanitize the message to remove ANSI escape codes @@ -143,11 +147,8 @@ public static void Log(LogLevel level, Exception exception, string message = nul } } - // Get the current process ID - int pid = Environment.ProcessId; - // Construct the log entry - string logEntry = $"{timestamp} [PID:{pid}] [{level}] [PodeMonitor] {logMessage}"; + string logEntry = $"{timestamp} [PID:{Environment.ProcessId}] [{level}] [PodeMonitor] {logMessage}"; // Thread-safe log file write lock (_logLock) @@ -158,7 +159,58 @@ public static void Log(LogLevel level, Exception exception, string message = nul } catch (Exception ex) { - Console.WriteLine($"Failed to log to file: {ex.Message}"); + 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 index dfe3a7516..d94633ffb 100644 --- a/src/PodeMonitor/PodeMonitorMain.cs +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -31,7 +31,7 @@ public static partial class Program 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. /// @@ -40,16 +40,42 @@ 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:PodeLogLevel").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 + } + + // 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.INFO); + PodeMonitorLogger.Initialize(logFilePath, logLevel, logMaxFileSize); + // Configure host builder var builder = CreateHostBuilder(args, customConfigFile); @@ -69,7 +95,7 @@ public static void Main(string[] args) } else { - PodeMonitorLogger.Log(LogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Unsupported platform. Exiting."); + PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, "Unsupported platform. Exiting."); } } @@ -94,7 +120,7 @@ private static IHostBuilder CreateHostBuilder(string[] args, string customConfig services.AddSingleton(serviceProvider => { var options = serviceProvider.GetRequiredService>().Value; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initializing PodeMonitor with options: {0}", JsonSerializer.Serialize(options)); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initializing PodeMonitor with options: {0}", JsonSerializer.Serialize(options)); return new PodeMonitor(options); }); @@ -162,25 +188,25 @@ private static void ConfigureWindows(IHostBuilder builder, string serviceName) private static void HandleSignalStop(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTSTP received."); HandlePause(); } private static void HandleSignalTerminate(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTERM received."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGTERM received."); HandleStop(); } private static void HandleSignalContinue(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGCONT received."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGCONT received."); HandleContinue(); } private static void HandleSignalRestart(int signum) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGHUP received."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "SIGHUP received."); HandleRestart(); } diff --git a/src/PodeMonitor/PodeMonitorWindowsService.cs b/src/PodeMonitor/PodeMonitorWindowsService.cs index 20b0424a0..b962e3c04 100644 --- a/src/PodeMonitor/PodeMonitorWindowsService.cs +++ b/src/PodeMonitor/PodeMonitorWindowsService.cs @@ -35,17 +35,17 @@ public PodeMonitorWindowsService(IHost host, string serviceName) /// Command-line arguments passed to the service. protected override void OnStart(string[] args) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service starting..."); + 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(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service started successfully."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service started successfully."); } catch (Exception ex) { // Log the exception to the custom log - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Service startup failed."); + 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}", @@ -70,19 +70,19 @@ protected override void OnStop() /// protected override void OnPause() { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service pausing..."); + 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(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnPause(); // Invoke the pause operation } else { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); } } @@ -91,19 +91,19 @@ protected override void OnPause() /// protected override void OnContinue() { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service resuming..."); + 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(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); + PodeMonitorLogger.Log(PodeLogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, $"Resolved IPausableHostedService: {service.GetType().FullName}"); ((IPausableHostedService)service).OnContinue(); // Invoke the resume operation } else { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService."); } } @@ -115,7 +115,7 @@ protected override void OnCustomCommand(int command) { if (command == CustomCommandRestart) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Custom restart command received. Restarting service..."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Custom restart command received. Restarting service..."); var service = _host.Services.GetService(typeof(IPausableHostedService)); if (service != null) { @@ -123,7 +123,7 @@ protected override void OnCustomCommand(int command) } else { - PodeMonitorLogger.Log(LogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService for restart."); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, "PodeMonitor", Environment.ProcessId, "Error: Failed to resolve IPausableHostedService for restart."); } } else diff --git a/src/PodeMonitor/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs index 397128956..21e516b32 100644 --- a/src/PodeMonitor/PodeMonitorWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -24,7 +24,7 @@ public sealed class PodeMonitorWorker : BackgroundService, IPausableHostedServic private bool _terminating = false; - public ServiceState State => _pwshMonitor.State; + public ServiceState State => _pwshMonitor.State; /// @@ -46,7 +46,7 @@ public PodeMonitorWorker(ILogger logger, PodeMonitor pwshMoni /// Cancellation token to signal when the worker should stop. protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "PodeMonitorWorker running at: {0}", DateTimeOffset.Now); + 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) @@ -61,12 +61,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) catch (Exception ex) { retryCount++; - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error in ExecuteAsync: {0}. Retry {1}/{2}", ex.Message, retryCount, _pwshMonitor.StartMaxRetryCount); + 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(LogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + PodeMonitorLogger.Log(PodeLogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); break; } @@ -78,7 +78,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Task.Delay(10000, stoppingToken); } - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Monitoring loop has stopped."); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Monitoring loop has stopped."); } /// @@ -91,7 +91,7 @@ public override async Task StopAsync(CancellationToken stoppingToken) await base.StopAsync(stoppingToken); // Wait for the base StopAsync to complete - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service stopped successfully at: {0}", DateTimeOffset.Now); } @@ -104,17 +104,17 @@ public void Shutdown() { _terminating = true; - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service is stopping at: {0}", DateTimeOffset.Now); try { _pwshMonitor.StopPowerShellProcess(); // Stop the process - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Stop message sent via pipe at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Stop message sent via pipe at: {0}", DateTimeOffset.Now); } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error stopping PowerShell process: {0}", ex.Message); } } } @@ -124,19 +124,19 @@ public void Shutdown() /// public void Restart() { - if ((!_terminating) && _pwshMonitor.State == ServiceState.Running || _pwshMonitor.State == ServiceState.Suspended) + if ((!_terminating) && _pwshMonitor.State == ServiceState.Running || _pwshMonitor.State == ServiceState.Suspended) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); try { _pwshMonitor.RestartPowerShellProcess(); // Restart the process - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Restart message sent via pipe at: {0}", DateTimeOffset.Now); + 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(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); } } } @@ -148,19 +148,19 @@ public void OnPause() { if ((!_terminating) && _pwshMonitor.State == ServiceState.Running) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); + 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(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Suspend message sent via pipe at: {0}", DateTimeOffset.Now); + 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 != ServiceState.Suspended) { if (retryCount >= 100) { - PodeMonitorLogger.Log(LogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + PodeMonitorLogger.Log(PodeLogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); break; } @@ -171,7 +171,7 @@ public void OnPause() } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error during pause: {0}", ex.Message); } } } @@ -183,19 +183,19 @@ public void OnContinue() { if ((!_terminating) && _pwshMonitor.State == ServiceState.Suspended) { - PodeMonitorLogger.Log(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); + 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(LogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Resume message sent via pipe at: {0}", DateTimeOffset.Now); + 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 == ServiceState.Suspended) { if (retryCount >= 100) { - PodeMonitorLogger.Log(LogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); + PodeMonitorLogger.Log(PodeLogLevel.CRITICAL, "PodeMonitor", Environment.ProcessId, "Maximum retry count reached. Exiting monitoring loop."); break; } @@ -208,7 +208,7 @@ public void OnContinue() } catch (Exception ex) { - PodeMonitorLogger.Log(LogLevel.ERROR, ex, "Error during continue: {0}", ex.Message); + PodeMonitorLogger.Log(PodeLogLevel.ERROR, ex, "Error during continue: {0}", ex.Message); } } } @@ -219,7 +219,7 @@ public void OnContinue() /// The name of the operation (e.g., "Pause" or "Resume"). private void AddOperationDelay(string operation) { - PodeMonitorLogger.Log(LogLevel.DEBUG, "PodeMonitor", Environment.ProcessId, "{0} operation completed. Adding delay of {1} ms.", operation, _delayMs); + 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 index 7eb7e830c..a83f5a490 100644 --- a/src/PodeMonitor/PodeMonitorWorkerOptions.cs +++ b/src/PodeMonitor/PodeMonitorWorkerOptions.cs @@ -8,7 +8,6 @@ namespace PodeMonitor /// public class PodeMonitorWorkerOptions { - /// /// The name of the service. /// @@ -36,6 +35,18 @@ public class PodeMonitorWorkerOptions /// 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. @@ -73,9 +84,10 @@ public class PodeMonitorWorkerOptions public override string ToString() { return $"Name: {Name}, ScriptPath: {ScriptPath}, PwshPath: {PwshPath}, ParameterString: {ParameterString}, " + - $"LogFilePath: {LogFilePath}, Quiet: {Quiet}, DisableTermination: {DisableTermination}, " + - $"ShutdownWaitTimeMs: {ShutdownWaitTimeMs}, StartMaxRetryCount: {StartMaxRetryCount}, " + - $"StartRetryDelayMs: {StartRetryDelayMs}"; + $"LogFilePath: {LogFilePath}, LogLevel: {LogLevel}, LogMaxFileSize: {LogMaxFileSize}, Quiet: {Quiet}, " + + $"DisableTermination: {DisableTermination}, ShutdownWaitTimeMs: {ShutdownWaitTimeMs}, " + + $"StartMaxRetryCount: {StartMaxRetryCount}, StartRetryDelayMs: {StartRetryDelayMs}"; } } + } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 47f3bf846..180f34f5f 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -4013,7 +4013,7 @@ function Convert-PodeMillisecondsToReadable { # Join with ":" and return return $components -join ':' } - + # Default or verbose format if ($VerboseOutput) { $verboseParts = @() @@ -4041,50 +4041,71 @@ function Convert-PodeMillisecondsToReadable { return $parts -join ':' } - <# .SYNOPSIS Determines the OS architecture for the current system. .DESCRIPTION - This function detects the operating system's architecture and converts it into a format - compatible with PowerShell installation requirements. It handles both Windows and Unix-based - systems and maps various architecture identifiers to PowerShell-supported names (e.g., 'x64', 'arm64'). + 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', or 'arm32'. + [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, the architecture is derived from the `PROCESSOR_ARCHITECTURE` environment variable. - - For Unix-based systems, the architecture is determined using the `uname -m` command. - - If the architecture is not supported, the function throws an exception. + - 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 architecture variable + # 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{ - # Architecture on Windows + } + else { + # For Windows, use the environment variable 'PROCESSOR_ARCHITECTURE' $arch = $env:PROCESSOR_ARCHITECTURE } - # Convert detected architecture to a PowerShell-compatible format + # Map the detected architecture to PowerShell-compatible formats switch ($arch.ToLowerInvariant()) { - 'amd64' { return 'x64' } # 64-bit architecture (AMD64) - 'x86' { return 'x86' } # 32-bit architecture - 'x86_64' { return 'x64' } # 64-bit architecture (x86_64) - 'armv7*' { return 'arm32' } # 32-bit ARM architecture - 'aarch64*' { return 'arm64' } # 64-bit ARM architecture - 'arm64' { return 'arm64' } # Explicit ARM64 - 'arm64*' { return 'arm64' } # Pattern matching for ARM64 - 'armv8*' { return 'arm64' } # ARM v8 series - default { throw "Unsupported architecture: $($arch)" } # Throw exception for unsupported architectures - } -} \ No newline at end of file + '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 +} diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 0f3e76299..1d7ddd270 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -63,7 +63,9 @@ function Start-PodeServiceHearthbeat { Write-PodeHost -Message "Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force Write-PodeHost -Message "Service State: $serviceState" -Force Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force - Write-PodeHost -Message "Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force + if ((Get-PodeServerUptime) -gt 1000) { + Write-PodeHost -Message "Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force + } Write-PodeHost -Message "Total Number of Restart: $(Get-PodeServerRestartCount)" -Force try { Start-Sleep -Milliseconds 100 @@ -1484,11 +1486,11 @@ function Get-PodeServiceStatus { if ($sudo) { $stateFilePath = "/Library/LaunchDaemons/PodeMonitor/$servicePid.state" - $plistPath="/Library/LaunchDaemons/$($nameService).plist" + $plistPath = "/Library/LaunchDaemons/$($nameService).plist" } else { $stateFilePath = "$($HOME)/Library/LaunchAgents/PodeMonitor/$servicePid.state" - $plistPath="$($HOME)/Library/LaunchAgents/$($nameService).plist" + $plistPath = "$($HOME)/Library/LaunchAgents/$($nameService).plist" } if (Test-Path -Path $stateFilePath) { $status = Get-Content -Path $stateFilePath -Raw diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index ffe47e970..1739ec8d6 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -5,10 +5,10 @@ .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. + 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. + 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." @@ -23,31 +23,31 @@ 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. + 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. + 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. + 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 by default is the current user (Windows only). + 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 by default is the current user (Linux Only). + Specifies the username under which the service will run on Linux. Defaults to the current user if not provided. .PARAMETER Agent - A switch to create an Agent instead of a Daemon in MacOS (MacOS Only). + A switch to 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 will be 'NT AUTHORITY\SYSTEM'. + 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). @@ -58,6 +58,12 @@ .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). + .EXAMPLE Register-PodeService -Name "PodeExampleService" -Description "Example Pode Service" -ParameterString "-Verbose" @@ -75,54 +81,79 @@ function Register-PodeService { [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] - $LogPath + [validateset('Debug', 'Info', 'Warn', 'Error', 'Critical')] + $LogLevel = 'Info', + + [Parameter()] + [Int64] + $LogMaxFileSize = 10 * 1024 * 1024 ) # Ensure the script is running with the necessary administrative/root privileges. @@ -199,6 +230,8 @@ function Register-PodeService { Name = $Name StartMaxRetryCount = $StartMaxRetryCount StartRetryDelayMs = $StartRetryDelayMs + LogLevel = $LogLevel.ToUpper() + LogMaxFileSize = $LogMaxFileSize } } @@ -208,6 +241,11 @@ function Register-PodeService { # 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' @@ -226,17 +264,6 @@ function Register-PodeService { $operation = Register-PodeMonitorWindowsService @param } elseif ($IsLinux) { - $musl = '' - if ($osArchitecture -eq 'x64') { - # Check for musl libc - if (Get-Command ldd -ErrorAction SilentlyContinue) { - $lddOutput = ldd --version 2>&1 - if ($lddOutput -match 'musl') { - $musl = 'musl-' - } - } - } - $param = @{ Name = $Name Description = $Description @@ -245,7 +272,7 @@ function Register-PodeService { User = $User Group = $Group Start = $Start - OsArchitecture = "linux-$($musl)$($osArchitecture)" + OsArchitecture = "linux-$osArchitecture" } $operation = Register-PodeLinuxService @param } From d2b5b49143bb988516ab93faada731ce527bccf0 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 28 Nov 2024 06:37:06 -0800 Subject: [PATCH 77/93] add full support for macos agents --- examples/HelloService/HelloService.ps1 | 24 ++--- src/Private/Service.ps1 | 135 ++++++++++++++++++------- src/Public/Service.ps1 | 94 +++++++++++++---- tests/integration/Service.Tests.ps1 | 50 +++++---- 4 files changed, 210 insertions(+), 93 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 5ed6ccf76..5f2b5fe37 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -21,8 +21,8 @@ .PARAMETER Password A secure password for the service account (Windows only). If omitted, the service account will be 'NT AUTHORITY\SYSTEM'. - .PARAMETER Daemon - Defines the service as an Daemon instead of a Agent.(macOS only) +.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. @@ -94,9 +94,9 @@ param( [securestring] $Password, - [Parameter(ParameterSetName = 'Register')] + [switch] - $Daemon, + $Agent, [Parameter(Mandatory = $true, ParameterSetName = 'Unregister')] [switch] @@ -153,33 +153,33 @@ catch { if ( $Register.IsPresent) { - return Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:(!$Daemon.IsPresent) + return Register-PodeService -Name $ServiceName -ParameterString "-Port $Port" -Password $Password -Agent:($Agent.IsPresent) } if ( $Unregister.IsPresent) { - return Unregister-PodeService -Name $ServiceName -Force:$Force + return Unregister-PodeService -Name $ServiceName -Force:$Force -Agent:($Agent.IsPresent) } if ($Start.IsPresent) { - return Start-PodeService -Name $ServiceName + return Start-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) } if ($Stop.IsPresent) { - return Stop-PodeService -Name $ServiceName + return Stop-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) } if ($Suspend.IsPresent) { - return Suspend-PodeService -Name $ServiceName + return Suspend-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) } if ($Resume.IsPresent) { - return Resume-PodeService -Name $ServiceName + return Resume-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) } if ($Query.IsPresent) { - return Get-PodeService -Name $ServiceName + return Get-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) } if ($Restart.IsPresent) { - return Restart-PodeService -Name $ServiceName + return Restart-PodeService -Name $ServiceName -Agent:($Agent.IsPresent) } # Start the Pode server diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 1d7ddd270..4f9ea3c3c 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -857,6 +857,9 @@ function Start-PodeLinuxService { .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`. @@ -880,6 +883,7 @@ function Test-PodeMacOsServiceIsRegistered { else { $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$nameService.plist" -PathType Leaf) } + if ($sudo) { $systemctlStatus = & sudo launchctl list $nameService 2>&1 } @@ -1058,6 +1062,9 @@ function Get-PodeMacOsServicePid { .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`. @@ -1069,11 +1076,21 @@ function Disable-PodeMacOsService { param( [Parameter(Mandatory = $true)] [string] - $Name + $Name, + + [switch] + $Agent ) # Standardize service naming for Linux/macOS $nameService = Get-PodeRealServiceName -Name $Name - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + + 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 } @@ -1096,6 +1113,9 @@ function Disable-PodeMacOsService { .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`. @@ -1107,10 +1127,13 @@ function Stop-PodeMacOsService { param( [Parameter(Mandatory = $true)] [string] - $Name + $Name, + + [switch] + $Agent ) - return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM) + return (Send-PodeServiceSignal -Name $Name -Signal SIGTERM -Agent:$Agent) } <# @@ -1124,6 +1147,9 @@ function Stop-PodeMacOsService { .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`. @@ -1137,10 +1163,20 @@ function Start-PodeMacOsService { param( [Parameter(Mandatory = $true)] [string] - $Name + $Name, + + [switch] + $Agent ) $nameService = Get-PodeRealServiceName -Name $Name - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + + 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 } @@ -1169,6 +1205,9 @@ function Start-PodeMacOsService { - `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`. @@ -1201,7 +1240,10 @@ function Send-PodeServiceSignal { [Parameter(Mandatory = $true)] [ValidateSet('SIGTSTP', 'SIGCONT', 'SIGHUP', 'SIGTERM')] [string] - $Signal + $Signal, + + [switch] + $Agent ) # Standardize service naming for Linux/macOS @@ -1225,7 +1267,7 @@ function Send-PodeServiceSignal { Write-Verbose -Message "Service '$Name' is active. Sending $Signal signal." # Retrieve service details, including the PID and privilege requirement - $svc = Get-PodeService -Name $Name + $svc = Get-PodeService -Name $Name -Agent:$Agent # Send the signal based on the privilege level if ($svc.Sudo) { @@ -1338,39 +1380,50 @@ function Wait-PodeServiceStatus { <# .SYNOPSIS - Retrieves the status of a Pode service across Windows, Linux, and macOS platforms. + Retrieves the status of a Pode service on Windows, Linux, and macOS. .DESCRIPTION - The `Get-PodeServiceStatus` function retrieves detailed information about a specified Pode service, including its current status, process ID (PID), and whether it requires elevated privileges (`Sudo`). The behavior varies based on the platform: - - Windows: Uses CIM to query the service status and maps standard states to Pode-specific states. - - Linux: Checks service status using `systemctl` and optionally reads additional state information from a custom state file. - - macOS: Uses `launchctl` and custom logic to determine the service's status and PID. + 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 - The name of the Pode service to query. + 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" + Get-PodeServiceStatus -Name "MyPodeService" + Retrieves the status of the Pode service named "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 - [hashtable] The function returns a hashtable with the following keys: - - Name: The service name. - - 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. + [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 - - Possible states: Running,Stopped,Suspended,Starting,Stopping,Pausing,Resuming,Unknown - - Requires administrative/root privileges to access service information on Linux and macOS. - - Platform-specific behaviors: - - **Windows**: Retrieves service information via the `Win32_Service` class. - - **Linux**: Uses `systemctl` to query the service status and retrieves custom Pode state if available. - - **macOS**: Uses `launchctl` to query service information and checks for custom Pode state files. - - If the service is not found, the function returns `$null`. - - Logs errors and warnings for troubleshooting. - - This is an internal function and may change in future releases of Pode. + - **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()] @@ -1378,7 +1431,10 @@ function Get-PodeServiceStatus { param ( [Parameter(Mandatory = $true)] [string] - $Name + $Name, + + [switch] + $Agent ) @@ -1404,6 +1460,7 @@ function Get-PodeServiceStatus { Pid = $service.ProcessId Sudo = $true PathName = $service.PathName + Type = 'Service' } } @@ -1462,6 +1519,7 @@ function Get-PodeServiceStatus { Pid = $servicePid Sudo = $true PathName = "/etc/systemd/system/$nameService" + Type = 'Service' } } else { @@ -1479,19 +1537,27 @@ function Get-PodeServiceStatus { try { $nameService = Get-PodeRealServiceName -Name $Name # Check if the service exists on macOS (launchctl) - if ((Test-PodeMacOsServiceIsRegistered $nameService )) { + if ((Test-PodeMacOsServiceIsRegistered $nameService -Agent:$Agent)) { $servicePid = Get-PodeMacOsServicePid -Name $nameService # Extract the PID from the match - $sudo = !(Test-Path -Path "$($HOME)/Library/LaunchAgents/$($nameService).plist" -PathType Leaf) + 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) @@ -1505,8 +1571,9 @@ function Get-PodeServiceStatus { Name = $Name Status = $status Pid = $servicePid - Sudo = $true + Sudo = $sudo PathName = $plistPath + Type = $serviceType } } else { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 1739ec8d6..501ab641e 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -324,6 +324,9 @@ function Register-PodeService { .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' @@ -358,14 +361,17 @@ function Start-PodeService { [Parameter(Mandatory = $false, ParameterSetName = 'Async')] [ValidateRange(1, 300)] - [int] $Timeout = 10 + [int] $Timeout = 10, + + [switch] + $Agent ) try { # Ensure administrative/root privileges Confirm-PodeAdminPrivilege # Get the service status - $service = Get-PodeServiceStatus -Name $Name + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent if (!$service) { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } @@ -404,7 +410,7 @@ function Start-PodeService { $serviceStarted = Start-PodeLinuxService -Name $Name } elseif ($IsMacOS) { - $serviceStarted = Start-PodeMacOsService -Name $Name + $serviceStarted = Start-PodeMacOsService -Name $Name -Agent:$Agent } # Check if the service start command failed @@ -448,6 +454,9 @@ function Start-PodeService { .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' @@ -486,14 +495,17 @@ function Stop-PodeService { [Parameter(Mandatory = $false, ParameterSetName = 'Async')] [ValidateRange(1, 300)] [int] - $Timeout = 10 + $Timeout = 10, + + [switch] + $Agent ) try { # Ensure administrative/root privileges Confirm-PodeAdminPrivilege # Get the service status - $service = Get-PodeServiceStatus -Name $Name + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent if (!$service) { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } @@ -528,7 +540,7 @@ function Stop-PodeService { $serviceStopped = Stop-PodeLinuxService -Name $Name } elseif ($IsMacOS) { - $serviceStopped = Stop-PodeMacOsService -Name $Name + $serviceStopped = Stop-PodeMacOsService -Name $Name -Agent:$Agent } if (!$serviceStopped) { @@ -569,6 +581,9 @@ function Stop-PodeService { .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' @@ -597,14 +612,17 @@ function Suspend-PodeService { [Parameter(Mandatory = $false, ParameterSetName = 'Async')] [ValidateRange(1, 300)] - [int] $Timeout = 10 + [int] $Timeout = 10, + + [switch] + $Agent ) try { # Ensure administrative/root privileges Confirm-PodeAdminPrivilege # Get the service status - $service = Get-PodeServiceStatus -Name $Name + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent if (!$service) { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } @@ -639,9 +657,12 @@ function Suspend-PodeService { if (Test-PodeIsWindows) { $serviceSuspended = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "pause '$Name'" } - elseif ($IsLinux -or $IsMacOS) { + 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) { @@ -682,6 +703,9 @@ function Suspend-PodeService { .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' @@ -710,14 +734,17 @@ function Resume-PodeService { [Parameter(Mandatory = $false, ParameterSetName = 'Async')] [ValidateRange(1, 300)] - [int] $Timeout = 10 + [int] $Timeout = 10, + + [switch] + $Agent ) try { # Ensure administrative/root privileges Confirm-PodeAdminPrivilege # Get the service status - $service = Get-PodeServiceStatus -Name $Name + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent if (!$service) { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } @@ -752,9 +779,12 @@ function Resume-PodeService { if (Test-PodeIsWindows) { $serviceResumed = Invoke-PodeWinElevatedCommand -Command 'sc.exe' -Arguments "continue '$Name'" } - elseif ($IsLinux -or $IsMacOS) { + 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) { @@ -794,6 +824,9 @@ function Resume-PodeService { .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 @@ -818,14 +851,17 @@ function Unregister-PodeService { [Parameter()] [switch] - $Force + $Force, + + [switch] + $Agent ) # Ensure administrative/root privileges Confirm-PodeAdminPrivilege # Get the service status - $service = Get-PodeServiceStatus -Name $Name + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent if (!$service) { throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) } @@ -836,7 +872,10 @@ function Unregister-PodeService { if ($service.Status -ne 'Stopped') { if ($Force) { Write-Verbose -Message "Service '$Name' is not stopped. Stopping the service due to -Force parameter." - Stop-PodeService -Name $Name + if (!(Stop-PodeService -Name $Name)) { + Write-Verbose -Message "Service '$Name' is not stopped." + return $false + } Write-Verbose -Message "Service '$Name' has been stopped." } else { @@ -900,7 +939,7 @@ function Unregister-PodeService { elseif ($IsMacOS) { # Disable and unregister the service - if (!(Disable-PodeMacOsService -Name $Name)) { + if (!(Disable-PodeMacOsService -Name $Name -Agent:$Agent)) { Write-Verbose -Message "Service '$Name' unregistered failed." throw ($PodeLocale.serviceUnRegistrationException -f $Name) } @@ -930,6 +969,7 @@ function Unregister-PodeService { } } } + return $true } } @@ -947,6 +987,9 @@ function Unregister-PodeService { .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. @@ -981,12 +1024,15 @@ function Get-PodeService { param( [Parameter(Mandatory = $true)] [string] - $Name + $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 + return Get-PodeServiceStatus -Name $Name -Agent:$Agent } <# @@ -1007,6 +1053,9 @@ function Get-PodeService { .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" @@ -1041,7 +1090,10 @@ function Restart-PodeService { [Parameter(Mandatory = $false, ParameterSetName = 'Async')] [int] - $Timeout = 10 + $Timeout = 10, + + [switch] + $Agent ) Write-Verbose -Message "Attempting to restart service '$Name' on platform $([System.Environment]::OSVersion.Platform)..." @@ -1051,7 +1103,7 @@ function Restart-PodeService { try { - $service = Get-PodeServiceStatus -Name $Name + $service = Get-PodeServiceStatus -Name $Name -Agent:$Agent if (!$service) { # Service is not registered throw ($PodeLocale.serviceIsNotRegisteredException -f $Name) @@ -1091,7 +1143,7 @@ function Restart-PodeService { } elseif ($IsMacOS) { # Start the service - if (((Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP'))) { + if (((Send-PodeServiceSignal -Name $Name -Signal 'SIGHUP' -Agent:$Agent))) { if ($Async) { return $true } diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 7c928d413..70804dcdd 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -6,11 +6,17 @@ param() Describe 'Service Lifecycle' { + BeforeAll { + $isAgent=$false + if ($IsMacOS){ + $isAgent=$true + } + } it 'register' { - $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Register -Agent:$isAgent $success | Should -BeTrue Start-Sleep 10 - $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent if ($IsMacOS) { $status.Status | Should -Be 'Running' $status.Pid | Should -BeGreaterThan 0 @@ -26,11 +32,11 @@ Describe 'Service Lifecycle' { it 'start' -Skip:( $IsMacOS) { - $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -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 + $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 @@ -38,11 +44,11 @@ Describe 'Service Lifecycle' { } it 'pause' { - $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend -Agent:$isAgent $success | Should -BeTrue Start-Sleep 2 # $webRequest = Invoke-WebRequest -uri http://localhost:8080 -ErrorAction SilentlyContinue - $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $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 @@ -50,21 +56,21 @@ Describe 'Service Lifecycle' { } it 'resume' { - $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -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 + $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 + $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop -Agent:$isAgent $success | Should -BeTrue Start-Sleep 2 - $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query + $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 @@ -73,11 +79,11 @@ Describe 'Service Lifecycle' { } it 're-start' { - $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -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 + $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 @@ -85,25 +91,17 @@ Describe 'Service Lifecycle' { } - it 're-stop' { - $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Stop - $success | Should -BeTrue - Start-Sleep 2 - - $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query - $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 'unregister' { - $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -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 + $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 From ac4cbb9643dc1b955f632770923e12058592a449 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 28 Nov 2024 07:25:56 -0800 Subject: [PATCH 78/93] fix pipename generator on mac/linux+ log errors --- src/PodeMonitor/PipeNameGenerator.cs | 2 +- src/PodeMonitor/PodeMonitor.cs | 4 ++-- src/PodeMonitor/PodeMonitorLogger.cs | 4 +++- src/PodeMonitor/PodeMonitorMain.cs | 9 ++++++--- src/Public/Core.ps1 | 1 + 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/PodeMonitor/PipeNameGenerator.cs b/src/PodeMonitor/PipeNameGenerator.cs index f7d2c3fa7..6a4299dba 100644 --- a/src/PodeMonitor/PipeNameGenerator.cs +++ b/src/PodeMonitor/PipeNameGenerator.cs @@ -21,7 +21,7 @@ public static string GeneratePipeName() { // Use Unix domain socket format with a shorter temp directory //string pipePath = Path.Combine(UnixTempDir, $"PodePipe_{uniqueId}"); - string pipePath = "PodePipe_{uniqueId}"; + string pipePath = $"PodePipe_{uniqueId}"; // Ensure the path is within the allowed length for Unix domain sockets if (pipePath.Length > MaxUnixPathLength) diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 18d30fbe3..334b3a707 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -71,7 +71,7 @@ public PodeMonitor(PodeMonitorWorkerOptions options) // Generate a unique pipe name _pipeName = PipeNameGenerator.GeneratePipeName(); - PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName}"); + // Define the state file path only for Linux/macOS if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { @@ -100,7 +100,7 @@ public PodeMonitor(PodeMonitorWorkerOptions options) _stateFilePath = Path.Combine(stateDirectory, $"{Environment.ProcessId}.state"); - PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Initialized PodeMonitor with pipe name: {_pipeName} and state file: {_stateFilePath}"); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initialized PodeMonitor with pipe name: {0} and state file: {1}",_pipeName,_stateFilePath); } } diff --git a/src/PodeMonitor/PodeMonitorLogger.cs b/src/PodeMonitor/PodeMonitorLogger.cs index c2be7bab3..35e841568 100644 --- a/src/PodeMonitor/PodeMonitorLogger.cs +++ b/src/PodeMonitor/PodeMonitorLogger.cs @@ -95,7 +95,9 @@ public static void Log(PodeLogLevel level, string context, int pid, string messa } catch (Exception ex) { - Console.WriteLine($"Failed to log to file: {ex.Message}"); + Console.WriteLine($"Failed to log to file:"); + Console.WriteLine($"{context} - {message}"); + Console.WriteLine($"Error: {ex.Message}"); } } diff --git a/src/PodeMonitor/PodeMonitorMain.cs b/src/PodeMonitor/PodeMonitorMain.cs index d94633ffb..d06672efc 100644 --- a/src/PodeMonitor/PodeMonitorMain.cs +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -58,12 +58,17 @@ public static void Main(string[] args) string logFilePath = config.GetSection("PodeMonitorWorker:logFilePath").Value ?? "PodeMonitorService.log"; // Parse log level - string logLevelString = config.GetSection("PodeMonitorWorker:PodeLogLevel").Value; + 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; @@ -72,11 +77,9 @@ public static void Main(string[] args) 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); diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index b5bd3f03d..5479b4e78 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -164,6 +164,7 @@ function Start-PodeServer { Quiet = $PodeService.Quiet PipeName = $PodeService.PipeName } + write-podehost $PodeService -Explode -Force } } From 5c8bb3df8d0f653190d4c1ae55b4df3615fc82ff Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 29 Nov 2024 05:28:44 -0800 Subject: [PATCH 79/93] Fix examples --- examples/HelloService/HelloService.ps1 | 1 - examples/HelloService/HelloServices.ps1 | 43 ++++++++++++------------- src/Public/Service.ps1 | 2 +- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/examples/HelloService/HelloService.ps1 b/examples/HelloService/HelloService.ps1 index 5f2b5fe37..678c8bb99 100644 --- a/examples/HelloService/HelloService.ps1 +++ b/examples/HelloService/HelloService.ps1 @@ -94,7 +94,6 @@ param( [securestring] $Password, - [switch] $Agent, diff --git a/examples/HelloService/HelloServices.ps1 b/examples/HelloService/HelloServices.ps1 index 5a9e72383..94b9730c3 100644 --- a/examples/HelloService/HelloServices.ps1 +++ b/examples/HelloService/HelloServices.ps1 @@ -13,7 +13,10 @@ .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. @@ -81,6 +84,9 @@ param( [securestring] $Password, + [switch] + $Agent, + [Parameter(Mandatory = $true, ParameterSetName = 'Unregister')] [switch] $Unregister, @@ -133,53 +139,46 @@ catch { # If there is any error during the module import, throw the error throw } -$services=@{ - 'HelloService1'=8081 - 'HelloService2'=8082 - 'HelloService3'=8083 +$services = @{ + 'HelloService1' = 8081 + 'HelloService2' = 8082 + 'HelloService3' = 8083 } if ( $Register.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Register-PodeService -Name $($_.Key) -ParameterString "-Port $($_.Value)" -Password $Password } - exit + return $services.GetEnumerator() | ForEach-Object { Register-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) -ParameterString "-Port $($_.Value)" -Password $Password } } if ( $Unregister.IsPresent) { - $services.GetEnumerator() | ForEach-Object { try{Unregister-PodeService -Name $($_.Key) -Force:$Force }catch{Write-Error -Exception $_.Exception}} - exit + return $services.GetEnumerator() | ForEach-Object { try { Unregister-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) -Force:$Force }catch { Write-Error -Exception $_.Exception } } + } if ($Start.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Start-PodeService -Name $($_.Key) } - exit + return $services.GetEnumerator() | ForEach-Object { Start-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } } if ($Stop.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Stop-PodeService -Name $($_.Key) } - exit + return $services.GetEnumerator() | ForEach-Object { Stop-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } } if ($Query.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) } - exit + return $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } } if ($Resume.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Resume-PodeService -Name $($_.Key) } - exit + return $services.GetEnumerator() | ForEach-Object { Resume-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } } if ($Query.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) } - exit + return $services.GetEnumerator() | ForEach-Object { Get-PodeService -Name $($_.Key) -Agent:($Agent.IsPresent) } } if ($Restart.IsPresent) { - $services.GetEnumerator() | ForEach-Object { Restart-PodeService -Name $($_.Key) } - exit + 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 + 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 diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 501ab641e..1d04589ba 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -41,7 +41,7 @@ Specifies the username under which the service will run on Linux. Defaults to the current user if not provided. .PARAMETER Agent - A switch to create an Agent instead of a Daemon on macOS (macOS only). + Create an Agent instead of a Daemon on macOS (macOS only). .PARAMETER Start A switch to start the service immediately after registration. From 717c8d883ca53eea034a2e1a447d4bf767de65ee Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 26 Jan 2025 08:49:22 -0800 Subject: [PATCH 80/93] first fixes --- src/Private/Helpers.ps1 | 108 -------- src/Public/Core.ps1 | 588 ---------------------------------------- src/Public/Endpoint.ps1 | 2 +- 3 files changed, 1 insertion(+), 697 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 8ac45e1da..4bc8a3d62 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -4097,114 +4097,6 @@ function Invoke-PodeWinElevatedCommand { return Invoke-Expression "$Command $Arguments" } -<# -.SYNOPSIS - Converts a duration in milliseconds into a human-readable time format. - -.DESCRIPTION - This function takes an input duration in milliseconds and converts it into - a readable time format. It supports multiple output styles, such as verbose - (detailed text), compact (`dd:hh:mm:ss`), and concise (short notation). - Optionally, milliseconds can be excluded from the output. - -.PARAMETER Milliseconds - The duration in milliseconds to be converted. - -.PARAMETER VerboseOutput - If specified, outputs a detailed, descriptive format (e.g., "1 day, 2 hours, 3 minutes"). - -.PARAMETER CompactOutput - If specified, outputs a compact format (e.g., "dd:hh:mm:ss"). - -.PARAMETER ExcludeMilliseconds - If specified, excludes milliseconds from the output. - -.EXAMPLE - Convert-PodeMillisecondsToReadable -Milliseconds 123456789 - - Output: - 1d 10h 17m 36s - -.EXAMPLE - Convert-PodeMillisecondsToReadable -Milliseconds 123456789 -VerboseOutput - - Output: - 1 day, 10 hours, 17 minutes, 36 seconds, 789 milliseconds - -.EXAMPLE - Convert-PodeMillisecondsToReadable -Milliseconds 123456789 -CompactOutput -ExcludeMilliseconds - - Output: - 01:10:17:36 - -.NOTES - This is an internal function and may change in future releases of Pode. -#> - -function Convert-PodeMillisecondsToReadable { - param ( - [Parameter(Mandatory)] - [long]$Milliseconds, - - [switch]$VerboseOutput, # Provide detailed descriptions - [switch]$CompactOutput, # Provide compact format like dd:hh:mm:ss or mm:ss:ms - [switch]$ExcludeMilliseconds # Exclude milliseconds from the output - ) - - $timeSpan = [timespan]::FromMilliseconds($Milliseconds) - - if ($CompactOutput) { - # Dynamically build compact format - $components = @() - - # Include days only if greater than 0 - if ($timeSpan.Days -gt 0) { $components += '{0:D2}' -f $timeSpan.Days } - - # Include hours only if greater than 0 or days are included - if ($timeSpan.Hours -gt 0 -or $components.Count -gt 0) { $components += '{0:D2}' -f $timeSpan.Hours } - - # Include minutes if relevant - if ($timeSpan.Minutes -gt 0 -or $components.Count -gt 0) { $components += '{0:D2}' -f $timeSpan.Minutes } - - # Add seconds as the final required time component - $components += '{0:D2}' -f $timeSpan.Seconds - - # Append milliseconds if not excluded - if (-not $ExcludeMilliseconds) { - $components[-1] += ':{0:D3}' -f $timeSpan.Milliseconds - } - - # Join with ":" and return - return $components -join ':' - } - - # Default or verbose format - if ($VerboseOutput) { - $verboseParts = @() - if ($timeSpan.Days -gt 0) { $verboseParts += "$($timeSpan.Days) day$(if ($timeSpan.Days -ne 1) { 's' })" } - if ($timeSpan.Hours -gt 0) { $verboseParts += "$($timeSpan.Hours) hour$(if ($timeSpan.Hours -ne 1) { 's' })" } - if ($timeSpan.Minutes -gt 0) { $verboseParts += "$($timeSpan.Minutes) minute$(if ($timeSpan.Minutes -ne 1) { 's' })" } - if ($timeSpan.Seconds -gt 0) { $verboseParts += "$($timeSpan.Seconds) second$(if ($timeSpan.Seconds -ne 1) { 's' })" } - if (-not $ExcludeMilliseconds -and $timeSpan.Milliseconds -gt 0) { - $verboseParts += "$($timeSpan.Milliseconds) millisecond$(if ($timeSpan.Milliseconds -ne 1) { 's' })" - } - - return $verboseParts -join ' ' - } - - # Default concise format - $parts = @() - if ($timeSpan.Days -gt 0) { $parts += "$($timeSpan.Days)d" } - if ($timeSpan.Hours -gt 0 -or $parts.Count -gt 0) { $parts += "$($timeSpan.Hours)h" } - if ($timeSpan.Minutes -gt 0 -or $parts.Count -gt 0) { $parts += "$($timeSpan.Minutes)m" } - if ($timeSpan.Seconds -gt 0 -or $parts.Count -gt 0) { $parts += "$($timeSpan.Seconds)s" } - if (-not $ExcludeMilliseconds -and $timeSpan.Milliseconds -gt 0 -or $parts.Count -gt 0) { - $parts += "$($timeSpan.Milliseconds)ms" - } - - return $parts -join ':' -} - <# .SYNOPSIS Determines the OS architecture for the current system. diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 5cb332ad3..276f6f40e 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -921,594 +921,6 @@ function Show-PodeGui { } } -<# -.SYNOPSIS -Bind an endpoint to listen for incoming Requests. - -.DESCRIPTION -Bind an endpoint to listen for incoming Requests. The endpoints can be HTTP, HTTPS, TCP or SMTP, with the option to bind certificates. - -.PARAMETER Address -The IP/Hostname of the endpoint (Default: localhost). - -.PARAMETER Port -The Port number of the endpoint. - -.PARAMETER Hostname -An optional hostname for the endpoint, specifying a hostname restricts access to just the hostname. - -.PARAMETER Protocol -The protocol of the supplied endpoint. - -.PARAMETER Certificate -The path to a certificate that can be use to enable HTTPS - -.PARAMETER CertificatePassword -The password for the certificate file referenced in Certificate - -.PARAMETER CertificateKey -A key file to be paired with a PEM certificate file referenced in Certificate - -.PARAMETER CertificateThumbprint -A certificate thumbprint to bind onto HTTPS endpoints (Windows). - -.PARAMETER CertificateName -A certificate subject name to bind onto HTTPS endpoints (Windows). - -.PARAMETER CertificateStoreName -The name of a certifcate store where a certificate can be found (Default: My) (Windows). - -.PARAMETER CertificateStoreLocation -The location of a certifcate store where a certificate can be found (Default: CurrentUser) (Windows). - -.PARAMETER X509Certificate -The raw X509 certificate that can be use to enable HTTPS - -.PARAMETER TlsMode -The TLS mode to use on secure connections, options are Implicit or Explicit (SMTP only) (Default: Implicit). - -.PARAMETER Name -An optional name for the endpoint, that can be used with other functions (Default: GUID). - -.PARAMETER RedirectTo -The Name of another Endpoint to automatically generate a redirect route for all traffic. - -.PARAMETER Description -A quick description of the Endpoint - normally used in OpenAPI. - -.PARAMETER Acknowledge -An optional Acknowledge message to send to clients when they first connect, for TCP and SMTP endpoints only. - -.PARAMETER SslProtocol -One or more optional SSL Protocols this endpoints supports. (Default: SSL3/TLS12 - Just TLS12 on MacOS). - -.PARAMETER CRLFMessageEnd -If supplied, TCP endpoints will expect incoming data to end with CRLF. - -.PARAMETER Force -Ignore Adminstrator checks for non-localhost endpoints. - -.PARAMETER SelfSigned -Create and bind a self-signed certifcate for HTTPS endpoints. - -.PARAMETER AllowClientCertificate -Allow for client certificates to be sent on requests. - -.PARAMETER PassThru -If supplied, the endpoint created will be returned. - -.PARAMETER LookupHostname -If supplied, a supplied Hostname will have its IP Address looked up from host file or DNS. - -.PARAMETER DualMode -If supplied, this endpoint will listen on both the IPv4 and IPv6 versions of the supplied -Address. -For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 address. - -.PARAMETER Default -If supplied, this endpoint will be the default one used for internally generating URLs. - -.EXAMPLE -Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http - -.EXAMPLE -Add-PodeEndpoint -Address localhost -Protocol Smtp - -.EXAMPLE -Add-PodeEndpoint -Address dev.pode.com -Port 8443 -Protocol Https -SelfSigned - -.EXAMPLE -Add-PodeEndpoint -Address 127.0.0.2 -Hostname dev.pode.com -Port 8443 -Protocol Https -SelfSigned - -.EXAMPLE -Add-PodeEndpoint -Address live.pode.com -Protocol Https -CertificateThumbprint '2A9467F7D3940243D6C07DE61E7FCCE292' -#> -function Add-PodeEndpoint { - [CmdletBinding(DefaultParameterSetName = 'Default')] - [OutputType([hashtable])] - param( - [Parameter()] - [string] - $Address = 'localhost', - - [Parameter()] - [int] - $Port = 0, - - [Parameter()] - [string] - $Hostname, - - [Parameter()] - [ValidateSet('Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')] - [string] - $Protocol, - - [Parameter(Mandatory = $true, ParameterSetName = 'CertFile')] - [string] - $Certificate = $null, - - [Parameter(ParameterSetName = 'CertFile')] - [string] - $CertificatePassword = $null, - - [Parameter(ParameterSetName = 'CertFile')] - [string] - $CertificateKey = $null, - - [Parameter(Mandatory = $true, ParameterSetName = 'CertThumb')] - [string] - $CertificateThumbprint, - - [Parameter(Mandatory = $true, ParameterSetName = 'CertName')] - [string] - $CertificateName, - - [Parameter(ParameterSetName = 'CertName')] - [Parameter(ParameterSetName = 'CertThumb')] - [System.Security.Cryptography.X509Certificates.StoreName] - $CertificateStoreName = 'My', - - [Parameter(ParameterSetName = 'CertName')] - [Parameter(ParameterSetName = 'CertThumb')] - [System.Security.Cryptography.X509Certificates.StoreLocation] - $CertificateStoreLocation = 'CurrentUser', - - [Parameter(Mandatory = $true, ParameterSetName = 'CertRaw')] - [X509Certificate] - $X509Certificate = $null, - - [Parameter(ParameterSetName = 'CertFile')] - [Parameter(ParameterSetName = 'CertThumb')] - [Parameter(ParameterSetName = 'CertName')] - [Parameter(ParameterSetName = 'CertRaw')] - [Parameter(ParameterSetName = 'CertSelf')] - [ValidateSet('Implicit', 'Explicit')] - [string] - $TlsMode = 'Implicit', - - [Parameter()] - [string] - $Name = $null, - - [Parameter()] - [string] - $RedirectTo = $null, - - [Parameter()] - [string] - $Description, - - [Parameter()] - [string] - $Acknowledge, - - [Parameter()] - [ValidateSet('Ssl2', 'Ssl3', 'Tls', 'Tls11', 'Tls12', 'Tls13')] - [string[]] - $SslProtocol = $null, - - [switch] - $CRLFMessageEnd, - - [switch] - $Force, - - [Parameter(ParameterSetName = 'CertSelf')] - [switch] - $SelfSigned, - - [switch] - $AllowClientCertificate, - - [switch] - $PassThru, - - [switch] - $LookupHostname, - - [switch] - $DualMode, - - [switch] - $Default - ) - - # error if serverless - Test-PodeIsServerless -FunctionName 'Add-PodeEndpoint' -ThrowError - - # if RedirectTo is supplied, then a Name is mandatory - if (![string]::IsNullOrWhiteSpace($RedirectTo) -and [string]::IsNullOrWhiteSpace($Name)) { - # A Name is required for the endpoint if the RedirectTo parameter is supplied - throw ($PodeLocale.nameRequiredForEndpointIfRedirectToSuppliedExceptionMessage) - } - - # get the type of endpoint - $type = Get-PodeEndpointType -Protocol $Protocol - - # are we running as IIS for HTTP/HTTPS? (if yes, force the port, address and protocol) - $isIIS = ((Test-PodeIsIIS) -and (@('Http', 'Ws') -icontains $type)) - if ($isIIS) { - $Port = [int]$env:ASPNETCORE_PORT - $Address = '127.0.0.1' - $Hostname = [string]::Empty - $Protocol = $type - } - - # are we running as Heroku for HTTP/HTTPS? (if yes, force the port, address and protocol) - $isHeroku = ((Test-PodeIsHeroku) -and (@('Http') -icontains $type)) - if ($isHeroku) { - $Port = [int]$env:PORT - $Address = '0.0.0.0' - $Hostname = [string]::Empty - $Protocol = $type - } - - # parse the endpoint for host/port info - if (![string]::IsNullOrWhiteSpace($Hostname) -and !(Test-PodeHostname -Hostname $Hostname)) { - # Invalid hostname supplied - throw ($PodeLocale.invalidHostnameSuppliedExceptionMessage -f $Hostname) - } - - if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) { - $Hostname = $Address - $Address = 'localhost' - } - - if (![string]::IsNullOrWhiteSpace($Hostname) -and $LookupHostname) { - $Address = (Get-PodeIPAddressesForHostname -Hostname $Hostname -Type All | Select-Object -First 1) - } - - $_endpoint = Get-PodeEndpointInfo -Address "$($Address):$($Port)" - - # if no name, set to guid, then check uniqueness - if ([string]::IsNullOrWhiteSpace($Name)) { - $Name = New-PodeGuid -Secure - } - - if ($PodeContext.Server.Endpoints.ContainsKey($Name)) { - # An endpoint named has already been defined - throw ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f $Name) - } - - # protocol must be https for client certs, or hosted behind a proxy like iis - if (($Protocol -ine 'https') -and !(Test-PodeIsHosted) -and $AllowClientCertificate) { - # Client certificates are only supported on HTTPS endpoints - throw ($PodeLocale.clientCertificatesOnlySupportedOnHttpsEndpointsExceptionMessage) - } - - # explicit tls is only supported for smtp/tcp - if (($type -inotin @('smtp', 'tcp')) -and ($TlsMode -ieq 'explicit')) { - # The Explicit TLS mode is only supported on SMTPS and TCPS endpoints - throw ($PodeLocale.explicitTlsModeOnlySupportedOnSmtpsTcpsEndpointsExceptionMessage) - } - - # ack message is only for smtp/tcp - if (($type -inotin @('smtp', 'tcp')) -and ![string]::IsNullOrEmpty($Acknowledge)) { - # The Acknowledge message is only supported on SMTP and TCP endpoints - throw ($PodeLocale.acknowledgeMessageOnlySupportedOnSmtpTcpEndpointsExceptionMessage) - } - - # crlf message end is only for tcp - if (($type -ine 'tcp') -and $CRLFMessageEnd) { - # The CRLF message end check is only supported on TCP endpoints - throw ($PodeLocale.crlfMessageEndCheckOnlySupportedOnTcpEndpointsExceptionMessage) - } - - # new endpoint object - $obj = @{ - Name = $Name - Description = $Description - DualMode = $DualMode - Address = $null - RawAddress = $null - Port = $null - IsIPAddress = $true - HostName = $Hostname - FriendlyName = $Hostname - Url = $null - Ssl = @{ - Enabled = (@('https', 'wss', 'smtps', 'tcps') -icontains $Protocol) - Protocols = $PodeContext.Server.Sockets.Ssl.Protocols - } - Protocol = $Protocol.ToLowerInvariant() - Type = $type.ToLowerInvariant() - Runspace = @{ - PoolName = (Get-PodeEndpointRunspacePoolName -Protocol $Protocol) - } - Default = $Default.IsPresent - Certificate = @{ - Raw = $X509Certificate - SelfSigned = $SelfSigned - AllowClientCertificate = $AllowClientCertificate - TlsMode = $TlsMode - } - Tcp = @{ - Acknowledge = $Acknowledge - CRLFMessageEnd = $CRLFMessageEnd - } - } - - # set ssl protocols - if (!(Test-PodeIsEmpty $SslProtocol)) { - $obj.Ssl.Protocols = (ConvertTo-PodeSslProtocol -Protocol $SslProtocol) - } - - # set the ip for the context (force to localhost for IIS) - $obj.Address = Get-PodeIPAddress $_endpoint.Host -DualMode:$DualMode - $obj.IsIPAddress = [string]::IsNullOrWhiteSpace($obj.HostName) - - if ($obj.IsIPAddress) { - if (!(Test-PodeIPAddressLocalOrAny -IP $obj.Address)) { - $obj.FriendlyName = "$($obj.Address)" - } - else { - $obj.FriendlyName = 'localhost' - } - } - - # set the port for the context, if 0 use a default port for protocol - $obj.Port = $_endpoint.Port - if (([int]$obj.Port) -eq 0) { - $obj.Port = Get-PodeDefaultPort -Protocol $Protocol -TlsMode $TlsMode - } - - if ($obj.IsIPAddress) { - $obj.RawAddress = "$($obj.Address):$($obj.Port)" - } - else { - $obj.RawAddress = "$($obj.FriendlyName):$($obj.Port)" - } - - # set the url of this endpoint - $obj.Url = "$($obj.Protocol)://$($obj.FriendlyName):$($obj.Port)/" - - # if the address is non-local, then check admin privileges - if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and !(Test-PodeIsAdminUser)) { - # Must be running with administrator privileges to listen on non-localhost addresses - throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) - } - - # has this endpoint been added before? (for http/https we can just not add it again) - $exists = ($PodeContext.Server.Endpoints.Values | Where-Object { - ($_.FriendlyName -ieq $obj.FriendlyName) -and ($_.Port -eq $obj.Port) -and ($_.Ssl.Enabled -eq $obj.Ssl.Enabled) -and ($_.Type -ieq $obj.Type) - } | Measure-Object).Count - - # if we're dealing with a certificate, attempt to import it - if (!(Test-PodeIsHosted) -and ($PSCmdlet.ParameterSetName -ilike 'cert*')) { - # fail if protocol is not https - if (@('https', 'wss', 'smtps', 'tcps') -inotcontains $Protocol) { - # Certificate supplied for non-HTTPS/WSS endpoint - throw ($PodeLocale.certificateSuppliedForNonHttpsWssEndpointExceptionMessage) - } - - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'certfile' { - $obj.Certificate.Raw = Get-PodeCertificateByFile -Certificate $Certificate -Password $CertificatePassword -Key $CertificateKey - } - - 'certthumb' { - $obj.Certificate.Raw = Get-PodeCertificateByThumbprint -Thumbprint $CertificateThumbprint -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation - } - - 'certname' { - $obj.Certificate.Raw = Get-PodeCertificateByName -Name $CertificateName -StoreName $CertificateStoreName -StoreLocation $CertificateStoreLocation - } - - 'certself' { - $obj.Certificate.Raw = New-PodeSelfSignedCertificate - } - } - - # fail if the cert is expired - if ($obj.Certificate.Raw.NotAfter -lt [datetime]::Now) { - # The certificate has expired - throw ($PodeLocale.certificateExpiredExceptionMessage -f $obj.Certificate.Raw.Subject, $obj.Certificate.Raw.NotAfter) - } - } - - if (!$exists) { - # set server type - $_type = $type - if ($_type -iin @('http', 'ws')) { - $_type = 'http' - } - - if ($PodeContext.Server.Types -inotcontains $_type) { - $PodeContext.Server.Types += $_type - } - - # add the new endpoint - $PodeContext.Server.Endpoints[$Name] = $obj - $PodeContext.Server.EndpointsMap["$($obj.Protocol)|$($obj.RawAddress)"] = $Name - } - - # if RedirectTo is set, attempt to build a redirecting route - if (!(Test-PodeIsHosted) -and ![string]::IsNullOrWhiteSpace($RedirectTo)) { - $redir_endpoint = $PodeContext.Server.Endpoints[$RedirectTo] - - # ensure the name exists - if (Test-PodeIsEmpty $redir_endpoint) { - # An endpoint named has not been defined for redirecting - throw ($PodeLocale.endpointNotDefinedForRedirectingExceptionMessage -f $RedirectTo) - } - - # build the redirect route - Add-PodeRoute -Method * -Path * -EndpointName $obj.Name -ArgumentList $redir_endpoint -ScriptBlock { - param($endpoint) - Move-PodeResponseUrl -EndpointName $endpoint.Name - } - } - - # return the endpoint? - if ($PassThru) { - return $obj - } -} - -<# -.SYNOPSIS -Get an Endpoint(s). - -.DESCRIPTION -Get an Endpoint(s). - -.PARAMETER Address -An Address to filter the endpoints. - -.PARAMETER Port -A Port to filter the endpoints. - -.PARAMETER Hostname -A Hostname to filter the endpoints. - -.PARAMETER Protocol -A Protocol to filter the endpoints. - -.PARAMETER Name -Any endpoints Names to filter endpoints. - -.EXAMPLE -Get-PodeEndpoint -Address 127.0.0.1 - -.EXAMPLE -Get-PodeEndpoint -Protocol Http - -.EXAMPLE -Get-PodeEndpoint -Name Admin, User -#> -function Get-PodeEndpoint { - [CmdletBinding()] - param( - [Parameter()] - [string] - $Address, - - [Parameter()] - [int] - $Port = 0, - - [Parameter()] - [string] - $Hostname, - - [Parameter()] - [ValidateSet('', 'Http', 'Https', 'Smtp', 'Smtps', 'Tcp', 'Tcps', 'Ws', 'Wss')] - [string] - $Protocol, - - [Parameter()] - [string[]] - $Name - ) - - if ((Test-PodeHostname -Hostname $Address) -and ($Address -inotin @('localhost', 'all'))) { - $Hostname = $Address - $Address = 'localhost' - } - - $endpoints = $PodeContext.Server.Endpoints.Values - - # if we have an address, filter - if (![string]::IsNullOrWhiteSpace($Address)) { - if (($Address -eq '*') -or $PodeContext.Server.IsHeroku) { - $Address = '0.0.0.0' - } - - if ($PodeContext.Server.IsIIS -or ($Address -ieq 'localhost')) { - $Address = '127.0.0.1' - } - - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Address.ToString() -ine $Address) { - continue - } - - $endpoint - }) - } - - # if we have a hostname, filter - if (![string]::IsNullOrWhiteSpace($Hostname)) { - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Hostname.ToString() -ine $Hostname) { - continue - } - - $endpoint - }) - } - - # if we have a port, filter - if ($Port -gt 0) { - if ($PodeContext.Server.IsIIS) { - $Port = [int]$env:ASPNETCORE_PORT - } - - if ($PodeContext.Server.IsHeroku) { - $Port = [int]$env:PORT - } - - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Port -ne $Port) { - continue - } - - $endpoint - }) - } - - # if we have a protocol, filter - if (![string]::IsNullOrWhiteSpace($Protocol)) { - if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) { - $Protocol = 'Http' - } - - $endpoints = @(foreach ($endpoint in $endpoints) { - if ($endpoint.Protocol -ine $Protocol) { - continue - } - - $endpoint - }) - } - - # further filter by endpoint names - if (($null -ne $Name) -and ($Name.Length -gt 0)) { - $endpoints = @(foreach ($_name in $Name) { - foreach ($endpoint in $endpoints) { - if ($endpoint.Name -ine $_name) { - continue - } - - $endpoint - } - }) - } - - # return - return $endpoints -} - <# .SYNOPSIS Sets the path for a specified default folder type in the Pode server context. diff --git a/src/Public/Endpoint.ps1 b/src/Public/Endpoint.ps1 index d6b1c97c1..04aeab786 100644 --- a/src/Public/Endpoint.ps1 +++ b/src/Public/Endpoint.ps1 @@ -365,7 +365,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) } From 285026c97052cab793f8f31cd7f5717e76a00696 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 26 Jan 2025 11:40:44 -0800 Subject: [PATCH 81/93] Fix a resume issue --- src/Private/CancellationToken.ps1 | 18 ++++++------------ src/Private/Context.ps1 | 4 +++- src/Private/Service.ps1 | 18 +++++++++--------- src/Public/Core.ps1 | 13 ++++++++----- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Private/CancellationToken.ps1 b/src/Private/CancellationToken.ps1 index 13fbd140d..9c23bdedd 100644 --- a/src/Private/CancellationToken.ps1 +++ b/src/Private/CancellationToken.ps1 @@ -418,10 +418,6 @@ function Test-PodeCancellationTokenRequest { its operations. It interacts with the Pode server's context and state to perform the necessary operations based on the allowed actions and current state. -.PARAMETER serverState - The current state of the Pode server, retrieved using Get-PodeServerState, - which determines whether actions like suspend, disable, or restart can be executed. - .NOTES This is an internal function and may change in future releases of Pode. @@ -431,12 +427,8 @@ function Test-PodeCancellationTokenRequest { #> function Resolve-PodeCancellationToken { - param( - [Parameter(Mandatory = $true)] - [Pode.PodeServerState] - $ServerState - ) - + #Retrieve the current state of the Pode server + $serverState = Get-PodeServerState if ($PodeContext.Server.AllowedActions.Restart -and (Test-PodeCancellationTokenRequest -Type Restart)) { Restart-PodeInternalServer return @@ -461,11 +453,13 @@ function Resolve-PodeCancellationToken { } # Handle suspend/resume actions if ($PodeContext.Server.AllowedActions.Suspend) { - if ((Test-PodeCancellationTokenRequest -Type Resume) -and ($ServerState -eq [Pode.PodeServerState]::Suspended)) { + if ((Test-PodeCancellationTokenRequest -Type Resume) -and ($ServerState -eq [Pode.PodeServerState]::Resuming)) { + # if ((Test-PodeCancellationTokenRequest -Type Resume) -and (($ServerState -eq [Pode.PodeServerState]::Suspended) -or ($ServerState -eq [Pode.PodeServerState]::Resuming))) { Resume-PodeServerInternal -Timeout $PodeContext.Server.AllowedActions.Timeout.Resume return } - elseif ((Test-PodeCancellationTokenRequest -Type Suspend) -and ($ServerState -eq [Pode.PodeServerState]::Running)) { + #elseif ((Test-PodeCancellationTokenRequest -Type Suspend) -and (($ServerState -eq [Pode.PodeServerState]::Running) -or ($ServerState -eq [Pode.PodeServerState]::Suspending))) { + elseif ((Test-PodeCancellationTokenRequest -Type Suspend) -and ($ServerState -eq [Pode.PodeServerState]::Suspending)) { Suspend-PodeServerInternal -Timeout $PodeContext.Server.AllowedActions.Timeout.Suspend return } diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 2472c4737..1a50334a4 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -222,7 +222,9 @@ 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) { @{} } + $ctx.Server.Configuration = if ($IgnoreServerConfig) { + @{} + } else { Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx -ConfigFile $ConfigFile } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 4f9ea3c3c..fc9c3e9bd 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -58,11 +58,11 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { $serviceState = 'running' - while (!$PodeContext.Tokens.Cancellation.IsCancellationRequested) { + while (!(Test-PodeCancellationTokenRequest -Type Terminate)) { Write-PodeHost -Message "Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force Write-PodeHost -Message "Service State: $serviceState" -Force - Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force + Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Format verbose -ExcludeMilliseconds)" -Force if ((Get-PodeServerUptime) -gt 1000) { Write-PodeHost -Message "Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force } @@ -88,7 +88,7 @@ function Start-PodeServiceHearthbeat { # Process incoming messages in a loop as long as the pipe is connected while ($pipeStream.IsConnected) { $message = $reader.ReadLine() # Read message from the pipe - if ( $PodeContext.Tokens.Cancellation.IsCancellationRequested) { + if ( Test-PodeCancellationTokenRequest -Type Terminate) { return } @@ -124,19 +124,19 @@ function Start-PodeServiceHearthbeat { 'suspend' { # Process 'suspend' message Write-PodeHost -Message 'Server requested suspend. Suspending Pode ...' -Force - Start-Sleep 5 $serviceState = 'suspended' - #Suspend-PodeServer # Suspend Pode server - # return # Exit the loop + Write-PodeHost -Message "Service State: $serviceState" -Force + Start-Sleep 1 + Suspend-PodeServer } 'resume' { # Process 'resume' message Write-PodeHost -Message 'Server requested resume. Resuming Pode ...' -Force - Start-Sleep 5 $serviceState = 'running' - #Resume-PodeServer # Resume Pode server - # return # Exit the loop + Write-PodeHost -Message "Service State: $serviceState" -Force + Start-Sleep 1 + Resume-PodeServer } } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 276f6f40e..3d6df8714 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -235,15 +235,18 @@ function Start-PodeServer { if ($PodeService) { if ($null -ne $PodeService.DisableTermination -or $null -ne $PodeService.Quiet -or - $null -ne $PodeService.PipeName + $null -ne $PodeService.PipeName -or + $null -ne $PodeService.DisableConsoleInput ) { $DisableTermination = [switch]$PodeService.DisableTermination $Quiet = [switch]$PodeService.Quiet + $DisableConsoleInput = [switch]$PodeService.DisableConsoleInput $monitorService = @{ - DisableTermination = $PodeService.DisableTermination - Quiet = $PodeService.Quiet - PipeName = $PodeService.PipeName + DisableTermination = $PodeService.DisableTermination + Quiet = $PodeService.Quiet + PipeName = $PodeService.PipeName + DisableConsoleInput = $PodeService.DisableConsoleInput } write-podehost $PodeService -Explode -Force } @@ -330,7 +333,7 @@ function Start-PodeServer { } # Resolve cancellation token requests (e.g., Restart, Enable/Disable, Suspend/Resume). - Resolve-PodeCancellationToken -ServerState $serverState + Resolve-PodeCancellationToken # Pause for 1 second before re-checking the state and processing the next action. Start-Sleep -Seconds 1 From e5317b813880fd11ebf3ec8295f29a7d6613cc25 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 27 Jan 2025 10:23:15 -0800 Subject: [PATCH 82/93] update --- src/PodeMonitor/PodeMonitor.cs | 38 ++++++++++++++------- src/PodeMonitor/PodeMonitorMain.cs | 2 +- src/PodeMonitor/PodeMonitorWorkerOptions.cs | 18 +++++++++- src/Private/Context.ps1 | 7 ++-- src/Public/Core.ps1 | 14 ++++++-- src/Public/Service.ps1 | 17 ++++++++- 6 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 334b3a707..95eb93a11 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -34,10 +34,7 @@ public class PodeMonitor 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 bool _quiet; // Indicates whether the process runs in quiet mode - private readonly bool _disableTermination; // Indicates whether termination is disabled private readonly int _shutdownWaitTimeMs; // Timeout for shutting down the process - private readonly string _pipeName; // Name of the named pipe for communication private readonly string _stateFilePath; // Path to the service state file private DateTime _lastLogTime; // Tracks the last time the process logged activity @@ -50,8 +47,19 @@ public class PodeMonitor public ServiceState State { get => _state; set => _state = value; } - public bool DisableTermination { get => _disableTermination; } + 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. @@ -63,15 +71,13 @@ public PodeMonitor(PodeMonitorWorkerOptions options) _scriptPath = options.ScriptPath; _pwshPath = options.PwshPath; _parameterString = options.ParameterString; - _quiet = options.Quiet; - _disableTermination = options.DisableTermination; _shutdownWaitTimeMs = options.ShutdownWaitTimeMs; StartMaxRetryCount = options.StartMaxRetryCount; StartRetryDelayMs = options.StartRetryDelayMs; - // Generate a unique pipe name - _pipeName = PipeNameGenerator.GeneratePipeName(); - + // 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()) { @@ -100,7 +106,7 @@ public PodeMonitor(PodeMonitorWorkerOptions options) _stateFilePath = Path.Combine(stateDirectory, $"{Environment.ProcessId}.state"); - PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initialized PodeMonitor with pipe name: {0} and state file: {1}",_pipeName,_stateFilePath); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initialized PodeMonitor with pipe name: {0} and state file: {1}", _serviceJson.PipeName, _stateFilePath); } } @@ -314,10 +320,18 @@ private void UpdateServiceState(ServiceState state) /// The PowerShell command string. private string BuildCommand() { - string podeServiceJson = $"{{\\\"DisableTermination\\\": {_disableTermination.ToString().ToLower()}, \\\"Quiet\\\": {_quiet.ToString().ToLower()}, \\\"PipeName\\\": \\\"{_pipeName}\\\"}}"; + 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. /// @@ -333,7 +347,7 @@ private bool InitializePipeClientWithRetry(int maxRetries = 3) { if (_pipeClient == null) { - _pipeClient = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut); + _pipeClient = new NamedPipeClientStream(".", _serviceJson.PipeName, PipeDirection.InOut); } if (!_pipeClient.IsConnected) diff --git a/src/PodeMonitor/PodeMonitorMain.cs b/src/PodeMonitor/PodeMonitorMain.cs index d06672efc..e0945d3fe 100644 --- a/src/PodeMonitor/PodeMonitorMain.cs +++ b/src/PodeMonitor/PodeMonitorMain.cs @@ -123,7 +123,7 @@ private static IHostBuilder CreateHostBuilder(string[] args, string customConfig services.AddSingleton(serviceProvider => { var options = serviceProvider.GetRequiredService>().Value; - PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initializing PodeMonitor with options: {0}", JsonSerializer.Serialize(options)); + PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Initializing PodeMonitor with options: {0}", options.ToString()); return new PodeMonitor(options); }); diff --git a/src/PodeMonitor/PodeMonitorWorkerOptions.cs b/src/PodeMonitor/PodeMonitorWorkerOptions.cs index a83f5a490..5849d85a8 100644 --- a/src/PodeMonitor/PodeMonitorWorkerOptions.cs +++ b/src/PodeMonitor/PodeMonitorWorkerOptions.cs @@ -77,6 +77,21 @@ public class PodeMonitorWorkerOptions /// 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. /// @@ -86,7 +101,8 @@ 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}"; + $"StartMaxRetryCount: {StartMaxRetryCount}, StartRetryDelayMs: {StartRetryDelayMs}" + + $"DisableConsoleInput: {IgnoreServerConfig}, IgnoreServerConfig: {IgnoreServerConfig}, ConfigFile: {ConfigFile}"; } } diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 1a50334a4..e8d273fb2 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -222,11 +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. @@ -714,6 +714,7 @@ function New-PodeRunspacePool { $PodeContext.RunspacePools.Service = @{ Pool = [runspacefactory]::CreateRunspacePool(1, 1, $PodeContext.RunspaceState, $Host) State = 'Waiting' + LastId = 0 } } } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 3d6df8714..52388a5aa 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' @@ -241,15 +243,21 @@ function Start-PodeServer { $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 - } + Write-PodeHost $PodeService -Explode -Force } } try { diff --git a/src/Public/Service.ps1 b/src/Public/Service.ps1 index 1d04589ba..a96831b6b 100644 --- a/src/Public/Service.ps1 +++ b/src/Public/Service.ps1 @@ -64,6 +64,12 @@ .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" @@ -153,7 +159,14 @@ function Register-PodeService { [Parameter()] [Int64] - $LogMaxFileSize = 10 * 1024 * 1024 + $LogMaxFileSize = 10 * 1024 * 1024, + + [Parameter()] + [string] + $ConfigFile, + + [switch] + $IgnoreServerConfig ) # Ensure the script is running with the necessary administrative/root privileges. @@ -232,6 +245,8 @@ function Register-PodeService { StartRetryDelayMs = $StartRetryDelayMs LogLevel = $LogLevel.ToUpper() LogMaxFileSize = $LogMaxFileSize + ConfigFile = $ConfigFile + IgnoreServerConfig = $IgnoreServerConfig.IsPresent } } From 5864d56378ecec9858539c3fbebd4ffe3e741dfd Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 28 Jan 2025 07:23:51 -0800 Subject: [PATCH 83/93] enum changes --- src/PodeMonitor/IPausableHostedService.cs | 2 +- src/PodeMonitor/PodeMonitor.cs | 45 ++++------------ src/PodeMonitor/PodeMonitorServiceState.cs | 53 +++++++++++++++++++ src/PodeMonitor/PodeMonitorWorker.cs | 14 ++--- src/PodeMonitor/PodeServiceStateExtensions.cs | 31 +++++++++++ 5 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 src/PodeMonitor/PodeMonitorServiceState.cs create mode 100644 src/PodeMonitor/PodeServiceStateExtensions.cs diff --git a/src/PodeMonitor/IPausableHostedService.cs b/src/PodeMonitor/IPausableHostedService.cs index 0395b23cf..ce0426fbe 100644 --- a/src/PodeMonitor/IPausableHostedService.cs +++ b/src/PodeMonitor/IPausableHostedService.cs @@ -21,6 +21,6 @@ public interface IPausableHostedService void Restart(); - public ServiceState State { get; } + public PodeMonitorServiceState State { get; } } } diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 95eb93a11..9dfed83d9 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -7,17 +7,6 @@ namespace PodeMonitor { - /// - /// Enum representing possible states of the Pode service. - /// - public enum ServiceState - { - Unknown, // State is unknown - Running, // Service is running - Suspended, // Service is suspended - Starting, // Service is starting - Stopping // Service is stopping - } /// /// Class responsible for managing and monitoring the Pode PowerShell process. @@ -42,10 +31,10 @@ public class PodeMonitor public int StartMaxRetryCount { get; } // Maximum number of retries for starting the process public int StartRetryDelayMs { get; } // Delay between retries in milliseconds - private volatile ServiceState _state; + private volatile PodeMonitorServiceState _state; - public ServiceState State { get => _state; set => _state = value; } + public PodeMonitorServiceState State { get => _state; set => _state = value; } public bool DisableTermination { get => _serviceJson.DisableTermination; } @@ -274,27 +263,13 @@ private void ParseServiceState(string output) if (output.StartsWith("Service State: ", StringComparison.OrdinalIgnoreCase)) { - string state = output["Service State: ".Length..].Trim().ToLowerInvariant(); + string state = output["Service State: ".Length..].Trim(); - switch (state) - { - case "running": - UpdateServiceState(ServiceState.Running); - break; - case "suspended": - UpdateServiceState(ServiceState.Suspended); - break; - case "starting": - UpdateServiceState(ServiceState.Starting); - break; - case "stopping": - UpdateServiceState(ServiceState.Stopping); - break; - default: - PodeMonitorLogger.Log(PodeLogLevel.WARN, "PodeMonitor", Environment.ProcessId, $"Unknown service state: {state}"); - UpdateServiceState(ServiceState.Unknown); - break; - } + // Convert the extracted string to a PodeMonitorServiceState enum + PodeMonitorServiceState parsedState = state.ToPodeMonitorServiceState(); + + // Update the service state + UpdateServiceState(parsedState); } } @@ -302,7 +277,7 @@ private void ParseServiceState(string output) /// Updates the internal state variables based on the provided service state. /// /// The new service state. - private void UpdateServiceState(ServiceState state) + private void UpdateServiceState(PodeMonitorServiceState state) { _state = state; PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Service state updated to: {state}"); @@ -407,7 +382,7 @@ private void WaitForProcessExit(int timeout) /// Writes the current service state to the state file. /// /// The service state to write. - private void WriteServiceStateToFile(ServiceState state) + private void WriteServiceStateToFile(PodeMonitorServiceState state) { lock (_syncLock) // Ensure thread-safe access { diff --git a/src/PodeMonitor/PodeMonitorServiceState.cs b/src/PodeMonitor/PodeMonitorServiceState.cs new file mode 100644 index 000000000..b3d65db8e --- /dev/null +++ b/src/PodeMonitor/PodeMonitorServiceState.cs @@ -0,0 +1,53 @@ +namespace PodeMonitor +{ + /// + /// Enum representing possible states of the Pode service. + /// + public enum PodeMonitorServiceState + { + Unknown, // State is unknown + Stopping, // Service is stopping + + + /// + /// The server has been completely terminated and is no longer running. + /// + Terminated, + + /// + /// The server is in the process of terminating and shutting down its operations. + /// + Terminating, + + /// + /// The server is resuming from a suspended state and is starting to run again. + /// + Resuming, + + /// + /// The server is in the process of suspending its operations. + /// + Suspending, + + /// + /// The server is currently suspended and not processing any requests. + /// + Suspended, + + /// + /// The server is in the process of restarting its operations. + /// + Restarting, + + /// + /// The server is starting its operations. + /// + Starting, + + /// + /// The server is running and actively processing requests. + /// + Running + } + +} \ No newline at end of file diff --git a/src/PodeMonitor/PodeMonitorWorker.cs b/src/PodeMonitor/PodeMonitorWorker.cs index 21e516b32..89dca3ac9 100644 --- a/src/PodeMonitor/PodeMonitorWorker.cs +++ b/src/PodeMonitor/PodeMonitorWorker.cs @@ -24,7 +24,7 @@ public sealed class PodeMonitorWorker : BackgroundService, IPausableHostedServic private bool _terminating = false; - public ServiceState State => _pwshMonitor.State; + public PodeMonitorServiceState State => _pwshMonitor.State; /// @@ -100,7 +100,7 @@ public override async Task StopAsync(CancellationToken stoppingToken) /// public void Shutdown() { - if ((!_terminating) && (_pwshMonitor.State == ServiceState.Running || _pwshMonitor.State == ServiceState.Suspended)) + if ((!_terminating) && (_pwshMonitor.State == PodeMonitorServiceState.Running || _pwshMonitor.State == PodeMonitorServiceState.Suspended)) { _terminating = true; @@ -124,7 +124,7 @@ public void Shutdown() /// public void Restart() { - if ((!_terminating) && _pwshMonitor.State == ServiceState.Running || _pwshMonitor.State == ServiceState.Suspended) + if ((!_terminating) && _pwshMonitor.State == PodeMonitorServiceState.Running || _pwshMonitor.State == PodeMonitorServiceState.Suspended) { PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Service restarting at: {0}", DateTimeOffset.Now); try @@ -146,7 +146,7 @@ public void Restart() /// public void OnPause() { - if ((!_terminating) && _pwshMonitor.State == ServiceState.Running) + if ((!_terminating) && _pwshMonitor.State == PodeMonitorServiceState.Running) { PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Pause command received at: {0}", DateTimeOffset.Now); @@ -156,7 +156,7 @@ public void OnPause() 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 != ServiceState.Suspended) + while (_pwshMonitor.State != PodeMonitorServiceState.Suspended) { if (retryCount >= 100) { @@ -181,7 +181,7 @@ public void OnPause() /// public void OnContinue() { - if ((!_terminating) && _pwshMonitor.State == ServiceState.Suspended) + if ((!_terminating) && _pwshMonitor.State == PodeMonitorServiceState.Suspended) { PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, "Continue command received at: {0}", DateTimeOffset.Now); @@ -191,7 +191,7 @@ public void OnContinue() 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 == ServiceState.Suspended) + while (_pwshMonitor.State == PodeMonitorServiceState.Suspended) { if (retryCount >= 100) { diff --git a/src/PodeMonitor/PodeServiceStateExtensions.cs b/src/PodeMonitor/PodeServiceStateExtensions.cs new file mode 100644 index 000000000..ff55daf20 --- /dev/null +++ b/src/PodeMonitor/PodeServiceStateExtensions.cs @@ -0,0 +1,31 @@ +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 (Enum.TryParse(stateString, true, out PodeMonitorServiceState result)) // true for case-insensitive + { + 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(); + } + } +} From 1c9d486fa9d489dd95d996328f490562cd4393fa Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 28 Jan 2025 07:47:49 -0800 Subject: [PATCH 84/93] states improvement --- src/PodeMonitor/PodeMonitorServiceState.cs | 10 +++---- src/PodeMonitor/PodeServiceStateExtensions.cs | 17 ++++++++++- src/Private/Service.ps1 | 28 +++++++++---------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/PodeMonitor/PodeMonitorServiceState.cs b/src/PodeMonitor/PodeMonitorServiceState.cs index b3d65db8e..43e4e1ffd 100644 --- a/src/PodeMonitor/PodeMonitorServiceState.cs +++ b/src/PodeMonitor/PodeMonitorServiceState.cs @@ -6,18 +6,16 @@ namespace PodeMonitor public enum PodeMonitorServiceState { Unknown, // State is unknown - Stopping, // Service is stopping - /// - /// The server has been completely terminated and is no longer running. + /// The server has been completely Stopped and is no longer running. /// - Terminated, + Stopped, /// - /// The server is in the process of terminating and shutting down its operations. + /// The server is in the process of Stopping and shutting down its operations. /// - Terminating, + Stopping, /// /// The server is resuming from a suspended state and is starting to run again. diff --git a/src/PodeMonitor/PodeServiceStateExtensions.cs b/src/PodeMonitor/PodeServiceStateExtensions.cs index ff55daf20..21096e295 100644 --- a/src/PodeMonitor/PodeServiceStateExtensions.cs +++ b/src/PodeMonitor/PodeServiceStateExtensions.cs @@ -11,10 +11,25 @@ public static class PodeServiceStateExtensions /// The corresponding PodeMonitorServiceState, or Unknown if parsing fails. public static PodeMonitorServiceState ToPodeMonitorServiceState(this string stateString) { - if (Enum.TryParse(stateString, true, out PodeMonitorServiceState result)) // true for case-insensitive + 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 } diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index fc9c3e9bd..04053e8f4 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -57,9 +57,13 @@ function Start-PodeServiceHearthbeat { # Define the script block for the client receiver, listens for commands via the named pipe $scriptBlock = { - $serviceState = 'running' 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) ) + Write-PodeHost -Message "Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force Write-PodeHost -Message "Service State: $serviceState" -Force Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Format verbose -ExcludeMilliseconds)" -Force @@ -99,10 +103,10 @@ function Start-PodeServiceHearthbeat { 'shutdown' { # Process 'shutdown' message Write-PodeHost -Message 'Server requested shutdown. Closing Pode ...' -Force - $serviceState = 'stopping' - Write-PodeHost -Message "Service State: $serviceState" -Force Close-PodeServer # Gracefully stop Pode server Start-Sleep 1 + Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force + Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force return # Exit the loop } @@ -110,13 +114,11 @@ function Start-PodeServiceHearthbeat { 'restart' { # Process 'restart' message Write-PodeHost -Message 'Server requested restart. Restarting Pode ...' -Force - - $serviceState = 'starting' - Write-PodeHost -Message "Service State: $serviceState" -Force - Start-Sleep 1 Restart-PodeServer # Restart Pode server - Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force Start-Sleep 1 + Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force + + Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force return # Exit the loop } @@ -124,19 +126,17 @@ function Start-PodeServiceHearthbeat { 'suspend' { # Process 'suspend' message Write-PodeHost -Message 'Server requested suspend. Suspending Pode ...' -Force - $serviceState = 'suspended' - Write-PodeHost -Message "Service State: $serviceState" -Force - Start-Sleep 1 Suspend-PodeServer + Start-Sleep 1 + Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force } 'resume' { # Process 'resume' message Write-PodeHost -Message 'Server requested resume. Resuming Pode ...' -Force - $serviceState = 'running' - Write-PodeHost -Message "Service State: $serviceState" -Force - Start-Sleep 1 Resume-PodeServer + Start-Sleep 1 + Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force } } From 609d71bcce56f296846ff8a6134766e48e05529f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 28 Jan 2025 13:40:54 -0800 Subject: [PATCH 85/93] udate service test to include suspend --- tests/integration/Service.Tests.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration/Service.Tests.ps1 b/tests/integration/Service.Tests.ps1 index 70804dcdd..573ace650 100644 --- a/tests/integration/Service.Tests.ps1 +++ b/tests/integration/Service.Tests.ps1 @@ -7,9 +7,9 @@ param() Describe 'Service Lifecycle' { BeforeAll { - $isAgent=$false - if ($IsMacOS){ - $isAgent=$true + $isAgent = $false + if ($IsMacOS) { + $isAgent = $true } } it 'register' { @@ -47,12 +47,12 @@ Describe 'Service Lifecycle' { $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Suspend -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 'Suspended' $status.Name | Should -Be 'Hello Service' $status.Pid | Should -BeGreaterThan 0 - # $webRequest | Should -BeNullOrEmpty + { Invoke-WebRequest -uri http://localhost:8080 } | Should -Throw } it 'resume' { @@ -95,7 +95,7 @@ Describe 'Service Lifecycle' { it 'unregister' { $status = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Query -Agent:$isAgent $status.Status | Should -Be 'Running' - $isAgent=$status.Type -eq 'Agent' + $isAgent = $status.Type -eq 'Agent' $success = & "$($PSScriptRoot)\..\..\examples\HelloService\HelloService.ps1" -Unregister -Force -Agent:$isAgent $success | Should -BeTrue Start-Sleep 2 From 752cbabe06f69f96bbdbe2d642b8747c3aadf5b7 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 29 Jan 2025 07:16:16 -0800 Subject: [PATCH 86/93] minor fixes --- src/PodeMonitor/PodeMonitor.cs | 2 +- src/Private/Server.ps1 | 2 +- src/Private/Service.ps1 | 65 ++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/PodeMonitor/PodeMonitor.cs b/src/PodeMonitor/PodeMonitor.cs index 9dfed83d9..446c050cb 100644 --- a/src/PodeMonitor/PodeMonitor.cs +++ b/src/PodeMonitor/PodeMonitor.cs @@ -181,7 +181,7 @@ public void StopPowerShellProcess() { if (InitializePipeClientWithRetry()) { - SendPipeMessage("shutdown"); + SendPipeMessage("stop"); PodeMonitorLogger.Log(PodeLogLevel.INFO, "PodeMonitor", Environment.ProcessId, $"Waiting for {_shutdownWaitTimeMs} milliseconds for Pode process to exit..."); WaitForProcessExit(_shutdownWaitTimeMs); diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 9556f4fbf..54594df67 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -195,7 +195,7 @@ function Start-PodeInternalServer { Show-PodeConsoleInfo # Start Service Monitor - Start-PodeServiceHearthbeat + Start-PodeServiceHeartbeat } catch { throw diff --git a/src/Private/Service.ps1 b/src/Private/Service.ps1 index 04053e8f4..3cff5daca 100644 --- a/src/Private/Service.ps1 +++ b/src/Private/Service.ps1 @@ -35,7 +35,7 @@ function Test-PodeServiceEnabled { The function takes no parameters. It retrieves the pipe name from the Pode service context. .EXAMPLE - Start-PodeServiceHearthbeat + Start-PodeServiceHeartbeat This command starts the Pode service monitoring and waits for 'shutdown' or 'restart' commands from the named pipe. @@ -45,13 +45,15 @@ function Test-PodeServiceEnabled { 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 'shutdown' message, the Pode server is stopped. + 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-PodeServiceHearthbeat { - +function Start-PodeServiceHeartbeat { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] + [CmdletBinding()] + param() # Check if the Pode service is enabled if (Test-PodeServiceEnabled) { @@ -64,13 +66,13 @@ function Start-PodeServiceHearthbeat { $serviceState = Get-PodeServerState }until(( [Pode.PodeServerState]::Running, [Pode.PodeServerState]::Suspended, [Pode.PodeServerState]::Terminating) -contains ( $serviceState) ) - Write-PodeHost -Message "Initialize Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force - Write-PodeHost -Message "Service State: $serviceState" -Force - Write-PodeHost -Message "Total Uptime: $(Get-PodeServerUptime -Total -Format verbose -ExcludeMilliseconds)" -Force + [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) { - Write-PodeHost -Message "Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)" -Force + [System.Console]::WriteLine("Uptime Since Last Restart: $(Get-PodeServerUptime -Readable -OutputType Verbose -ExcludeMilliseconds)") } - Write-PodeHost -Message "Total Number of Restart: $(Get-PodeServerRestartCount)" -Force + [System.Console]::WriteLine("Total Number of Restart: $(Get-PodeServerRestartCount)") try { Start-Sleep -Milliseconds 100 # Create a named pipe server stream @@ -82,84 +84,85 @@ function Start-PodeServiceHearthbeat { [System.IO.Pipes.PipeOptions]::None ) - Write-PodeHost -Message "Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe." -Force + [System.Console]::WriteLine("Waiting for connection to the $($PodeContext.Server.Service.PipeName) pipe.") $pipeStream.WaitForConnection() # Wait until a client connects - Write-PodeHost -Message "Connected to the $($PodeContext.Server.Service.PipeName) pipe." -Force + [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 - while ($pipeStream.IsConnected) { + if ($pipeStream.IsConnected) { $message = $reader.ReadLine() # Read message from the pipe if ( Test-PodeCancellationTokenRequest -Type Terminate) { return } if ($message) { - Write-PodeHost -Message "Received message: $message" -Force + [System.Console]::WriteLine("Received message: $message") switch ($message) { - 'shutdown' { + 'stop' { # Process 'shutdown' message - Write-PodeHost -Message 'Server requested shutdown. Closing Pode ...' -Force + [System.Console]::WriteLine("Server request: 'Stop'. Closing Pode ...") Close-PodeServer # Gracefully stop Pode server Start-Sleep 1 - Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") - Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force + [System.Console]::WriteLine('Closing Service Monitoring Heartbeat') return # Exit the loop } 'restart' { # Process 'restart' message - Write-PodeHost -Message 'Server requested restart. Restarting Pode ...' -Force + [System.Console]::WriteLine("Server request: 'Restart'. Restarting Pode ...") Restart-PodeServer # Restart Pode server Start-Sleep 1 - Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") - Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force - return - # Exit the loop + [System.Console]::WriteLine('Closing Service Monitoring Heartbeat') + return # Exit the loop } 'suspend' { # Process 'suspend' message - Write-PodeHost -Message 'Server requested suspend. Suspending Pode ...' -Force + [System.Console]::WriteLine("Server request: 'Suspend'. Suspending Pode ...") Suspend-PodeServer Start-Sleep 1 - Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") + break } 'resume' { # Process 'resume' message - Write-PodeHost -Message 'Server requested resume. Resuming Pode ...' -Force + [System.Console]::WriteLine("Server request: 'Resume'. Resuming Pode ...") Resume-PodeServer Start-Sleep 1 - Write-PodeHost -Message "Service State: $(Get-PodeServerState)" -Force + [System.Console]::WriteLine("Service State: $(Get-PodeServerState)") + break } } } - break } } catch { $_ | Write-PodeErrorLog # Log any errors that occur during pipe operation - throw $_ } finally { if ($reader) { $reader.Dispose() } - if ( $pipeStream) { + if ($pipeStream) { + $pipeStream.Flush() + $pipeStream.Close() $pipeStream.Dispose() # Always dispose of the pipe stream when done - Write-PodeHost -Message "Disposing Listener Pipe $($PodeContext.Server.Service.PipeName)" -Force + [System.Console]::WriteLine("Disposing Listener Pipe $($PodeContext.Server.Service.PipeName)") } } } - Write-PodeHost -Message 'Closing Service Monitoring Heartbeat' -Force + [System.Console]::WriteLine('Closing Service Monitoring Heartbeat') } # Assign a name to the Pode service From 2c22236b0f1e6c06968cb85a5ddb98e2fc47683f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 21 Mar 2025 06:11:54 -0700 Subject: [PATCH 87/93] new Test-PodeBindToPrivilegedPort --- src/Locales/ar/Pode.psd1 | 1 + src/Locales/de/Pode.psd1 | 1 + src/Locales/en-us/Pode.psd1 | 1 + src/Locales/en/Pode.psd1 | 1 + src/Locales/es/Pode.psd1 | 1 + src/Locales/fr/Pode.psd1 | 1 + src/Locales/it/Pode.psd1 | 1 + src/Locales/ja/Pode.psd1 | 1 + src/Locales/ko/Pode.psd1 | 1 + src/Locales/nl/Pode.psd1 | 1 + src/Locales/pl/Pode.psd1 | 1 + src/Locales/pt/Pode.psd1 | 1 + src/Locales/zh/Pode.psd1 | 1 + src/Private/Helpers.ps1 | 116 +++++++++++++++++++++++++++++++++++- src/Public/Endpoint.ps1 | 6 +- 15 files changed, 130 insertions(+), 5 deletions(-) diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 1ee11608d..64130cf4d 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى للمعدل غير موجودة: {0}' accessLimitRuleAlreadyExistsExceptionMessage = 'تم تعريف قاعدة الحد الأقصى للوصول بالفعل: {0}' accessLimitRuleDoesNotExistExceptionMessage = 'قاعدة الحد الأقصى للوصول غير موجودة: {0}' + cannotBindPortInUseExceptionMessage = 'لا يمكن الربط مع {0}:{1} — المنفذ مستخدم بالفعل من قبل عملية أخرى.' } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index f74d1a653..a395d756f 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "Die Rate-Limit-Regel mit dem Namen '{0}' existiert nicht." accessLimitRuleAlreadyExistsExceptionMessage = "Die Zugriffsbeschränkungsregel mit dem Namen '{0}' existiert bereits." accessLimitRuleDoesNotExistExceptionMessage = "Die Zugriffsbeschränkungsregel mit dem Namen '{0}' existiert nicht." + cannotBindPortInUseExceptionMessage = 'Kann nicht an {0}:{1} binden — der Port wird bereits von einem anderen Prozess verwendet.' } \ No newline at end of file diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index ebaa0c836..052886687 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "A rate limit rule with the name '{0}' does not exist." accessLimitRuleAlreadyExistsExceptionMessage = "An access limit rule with the name '{0}' already exists." accessLimitRuleDoesNotExistExceptionMessage = "An access limit rule with the name '{0}' does not exist." + cannotBindPortInUseExceptionMessage = "Cannot bind to {0}:{1} — the port is already in use by another process." } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index f92fd4292..b537b6491 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "A Rate Limit Rule with the name '{0}' does not exist." accessLimitRuleAlreadyExistsExceptionMessage = "An Access Limit Rule with the name '{0}' already exists." accessLimitRuleDoesNotExistExceptionMessage = "An Access Limit Rule with the name '{0}' does not exist." + cannotBindPortInUseExceptionMessage = 'Cannot bind to {0}:{1} — the port is already in use by another process.' } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 8818d8a1f..302801a1c 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "La regla de límite de velocidad con el nombre '{0}' no existe." accessLimitRuleAlreadyExistsExceptionMessage = "La regla de límite de acceso con el nombre '{0}' ya existe." accessLimitRuleDoesNotExistExceptionMessage = "La regla de límite de acceso con el nombre '{0}' no existe." + cannotBindPortInUseExceptionMessage = 'No se puede enlazar a {0}:{1} — el puerto ya está siendo utilizado por otro proceso.' } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index a217413a1..56b341363 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "La règle de limite de taux '{0}' n'existe pas." accessLimitRuleAlreadyExistsExceptionMessage = "Une règle de limite d'accès nommée '{0}' existe déjà." accessLimitRuleDoesNotExistExceptionMessage = "La règle de limite d'accès '{0}' n'existe pas." + cannotBindPortInUseExceptionMessage = 'Impossible de se lier à {0}:{1} — le port est déjà utilisé par un autre processus.' } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 9049238d2..6773ea0ee 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "La regola di limitazione del tasso con il nome '{0}' non esiste." accessLimitRuleAlreadyExistsExceptionMessage = "Una regola di limitazione dell'accesso con il nome '{0}' esiste già." accessLimitRuleDoesNotExistExceptionMessage = "La regola di limitazione dell'accesso con il nome '{0}' non esiste." + cannotBindPortInUseExceptionMessage = 'Impossibile associare {0}:{1} — la porta è già in uso da un altro processo.' } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index 2d54d26f6..0c0df7932 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "名前が '{0}' のレート制限ルールは存在しません。" accessLimitRuleAlreadyExistsExceptionMessage = "名前が '{0}' のアクセス制限ルールは既に存在します。" accessLimitRuleDoesNotExistExceptionMessage = "名前が '{0}' のアクセス制限ルールは存在しません。" + cannotBindPortInUseExceptionMessage = '{0}:{1} にバインドできません — そのポートは他のプロセスによって使用されています。' } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index 9d55a61a3..40f7bbfe0 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "이름이 '{0}'인 비율 제한 규칙이 존재하지 않습니다." accessLimitRuleAlreadyExistsExceptionMessage = "이름이 '{0}'인 액세스 제한 규칙이 이미 존재합니다." accessLimitRuleDoesNotExistExceptionMessage = "이름이 '{0}'인 액세스 제한 규칙이 존재하지 않습니다." + cannotBindPortInUseExceptionMessage = '{0}:{1}에 바인딩할 수 없습니다 — 해당 포트는 다른 프로세스에서 이미 사용 중입니다.' } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index d297fa58e..dd66371c0 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "Rate Limit-regel met de naam '{0}' bestaat niet." accessLimitRuleAlreadyExistsExceptionMessage = "Toegangslimietregel met de naam '{0}' bestaat al." accessLimitRuleDoesNotExistExceptionMessage = "Toegangslimietregel met de naam '{0}' bestaat niet." + cannotBindPortInUseExceptionMessage = 'Kan geen verbinding maken met {0}:{1} — de poort wordt al gebruikt door een ander proces.' } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index 268ad1dbf..b66dc7001 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "Reguła limitu szybkości o nazwie '{0}' nie istnieje." accessLimitRuleAlreadyExistsExceptionMessage = "Reguła limitu dostępu o nazwie '{0}' już istnieje." accessLimitRuleDoesNotExistExceptionMessage = "Reguła limitu dostępu o nazwie '{0}' nie istnieje." + cannotBindPortInUseExceptionMessage = 'Nie można powiązać z {0}:{1} — port jest już używany przez inny proces.' } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index cc3041e32..9c61321fa 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = "A regra de limite de taxa com o nome '{0}' não existe." accessLimitRuleAlreadyExistsExceptionMessage = "A regra de limite de acesso com o nome '{0}' já existe." accessLimitRuleDoesNotExistExceptionMessage = "A regra de limite de acesso com o nome '{0}' não existe." + cannotBindPortInUseExceptionMessage = 'Não é possível vincular a {0}:{1} — a porta já está em uso por outro processo.' } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index d98d66f5b..c2c424ce6 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -334,4 +334,5 @@ rateLimitRuleDoesNotExistExceptionMessage = '速率限制规则不存在: {0}' accessLimitRuleAlreadyExistsExceptionMessage = '访问限制规则已存在: {0}' accessLimitRuleDoesNotExistExceptionMessage = '访问限制规则不存在: {0}' + cannotBindPortInUseExceptionMessage = '无法绑定到 {0}:{1} — 该端口已被其他进程占用。' } \ No newline at end of file diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 7de5d806d..3f977bf4b 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -3992,7 +3992,6 @@ function Test-PodeIsISEHost { if the -Elevate switch is used. - This is an internal function and may change in future releases of Pode. #> - function Test-PodeAdminPrivilege { param( [switch] @@ -4201,3 +4200,118 @@ function Get-PodeOSPwshArchitecture { # Return the final architecture string return $arch } + +<# +.SYNOPSIS + Tests whether the current session can bind to one or more privileged ports. + +.DESCRIPTION + Attempts to bind to each port in the specified list using the provided IP address. + + Behavior: + - Returns $true if any port can be successfully bound. + - Returns $false if a privilege error (AccessDenied) occurs and $CheckAdmin is enabled. + - Returns $false if all test ports are in use but no privilege error occurs. + - If -ThrowError is used: + • Throws a localized exception if a privilege error is detected and -CheckAdmin is also set. + • Throws a custom SocketException with a descriptive message when the port is already in use. + • Throws the raw exception for all other socket or unexpected errors. + +.PARAMETER IP + The IP address to bind to. Defaults to the loopback address (127.0.0.1). + +.PARAMETER Port + A single port number or an array of ports to test. Defaults to a set of typically unused privileged ports. + +.PARAMETER ThrowError + If specified, exceptions will be thrown instead of returning values for error conditions. + +.PARAMETER CheckAdmin + If specified, only privilege-related binding failures will result in a return value of $false; + otherwise, AccessDenied will return $true (to allow non-admin flows to continue). + +.OUTPUTS + [bool] $true — Binding was successful on at least one port. + [bool] $false — Privilege error occurred (with CheckAdmin), or a single port is in use. + [bool] $false — All ports were in use, but no privilege issue was detected. + +.EXAMPLE + Test-PodeBindToPrivilegedPort + +.EXAMPLE + Test-PodeBindToPrivilegedPort -IP '0.0.0.0' -Port 80 + +.EXAMPLE + Test-PodeBindToPrivilegedPort -ThrowError -CheckAdmin +#> + +function Test-PodeBindToPrivilegedPort { + [CmdletBinding()] + param ( + [Parameter()] + [string]$IP = '127.0.0.1', + + [Parameter()] + [int[]]$Port = @(1, 7, 9, 13, 19, 37, 79, 100), + + [switch] + $ThrowError, + + [switch] + $CheckAdmin + ) + + foreach ($p in $Port) { + try { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse($IP), $p) + $listener.Start() + $listener.Stop() + Write-Verbose "Successfully bound to $($IP):$p" + return $true + } + catch [System.Net.Sockets.SocketException] { + switch ($_.Exception.SocketErrorCode) { + 'AccessDenied' { + Write-Verbose "Access denied on $($IP):$p" + if ($ThrowError) { + if (!$CheckAdmin) { return } + throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) + } + if ($CheckAdmin) { + return $false + } + return $true + + } + 'AddressAlreadyInUse' { + Write-Verbose "Port $p is already in use on $IP" + + if ($Port.Count -gt 1) { + continue + } + if ($ThrowError) { + throw ($PodeLocale.cannotBindPortInUseExceptionMessage -f $IP,$p) + } + return $false + } + default { + Write-Verbose "Unhandled socket error on $($IP):$p — $($_.Exception.SocketErrorCode)" + if ($ThrowError) { + throw $_ + } + return $false + } + } + } + catch { + Write-Verbose "Unexpected error on $($IP):$p — $($_.Exception.Message)" + if ($ThrowError) { + throw $_ + } + return $false + } + } + + Write-Verbose "All test ports on $IP were in use, but no privilege error detected" + return $false +} diff --git a/src/Public/Endpoint.ps1 b/src/Public/Endpoint.ps1 index 04aeab786..574c017ab 100644 --- a/src/Public/Endpoint.ps1 +++ b/src/Public/Endpoint.ps1 @@ -365,10 +365,8 @@ 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-PodeAdminPrivilege -Console)) { - # Must be running with administrator privileges to listen on non-localhost addresses - throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) - } + #if (!$Force -and !(Test-PodeIPAddressLocal -IP $obj.Address) -and ! + $null = Test-PodeBindToPrivilegedPort -IP $obj.Address -Port $obj.Port -ThrowError -CheckAdmin:(!$Force.IsPresent) # has this endpoint been added before? (for http/https we can just not add it again) $exists = ($PodeContext.Server.Endpoints.Values | Where-Object { From 192bbf6f6489e6f50174ecbaa6e5e7bf71183b54 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 19 Apr 2025 10:19:34 -0700 Subject: [PATCH 88/93] change port 80 to 36842 --- tests/unit/Context.Tests.ps1 | 98 ++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 51330e98f..eae985e68 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -96,7 +96,7 @@ Describe 'Add-PodeEndpoint' { It 'Set both the Hostname address and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address 'foo.com' -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'foo.com' -Port 36842 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -110,7 +110,7 @@ Describe 'Add-PodeEndpoint' { It 'Set all the Hostname, ip and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.2' -Hostname 'foo.com' -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.2' -Hostname 'foo.com' -Port 36842 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -168,7 +168,7 @@ Describe 'Add-PodeEndpoint' { It 'Set just a port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Port 36842 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -182,7 +182,7 @@ Describe 'Add-PodeEndpoint' { It 'Set just a port with colon' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Port 36842 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -196,7 +196,7 @@ Describe 'Add-PodeEndpoint' { It 'Set both IPv4 address and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -210,7 +210,7 @@ Describe 'Add-PodeEndpoint' { It 'Set both IPv4 address and port for all' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '*' -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Address '*' -Port 36842 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -234,7 +234,7 @@ Describe 'Add-PodeEndpoint' { It 'Throws error for an invalid IPv4 address with port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '256.0.0.1' -Port 80 -Protocol 'HTTP' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '256.0.0.1:80' ) #'*Failed to parse*' + { Add-PodeEndpoint -Address '256.0.0.1' -Port 36842 -Protocol 'HTTP' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '256.0.0.1:80' ) #'*Failed to parse*' $PodeContext.Server.Types | Should -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 0 @@ -242,8 +242,8 @@ Describe 'Add-PodeEndpoint' { It 'Add two endpoints to listen on, of the same type' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - $ep1 = (Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -PassThru) - $ep2 = (Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -PassThru) + $ep1 = (Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -PassThru) + $ep2 = (Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -PassThru) $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -262,8 +262,8 @@ Describe 'Add-PodeEndpoint' { It 'Add two endpoints to listen on, with different names' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Example1' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'Example2' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Example1' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'Example2' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -284,8 +284,8 @@ Describe 'Add-PodeEndpoint' { It 'Add two endpoints to listen on, one of HTTP and one of HTTPS' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Http' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTPS' -Name 'Https' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Http' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTPS' -Name 'Https' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -304,8 +304,8 @@ Describe 'Add-PodeEndpoint' { It 'Add two endpoints to listen on, but one added as they are the same' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -319,15 +319,15 @@ Describe 'Add-PodeEndpoint' { It 'Allows adding two endpoints of different types' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'SMTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'SMTP' $PodeContext.Server.Endpoints.Count | Should -Be 2 } It 'Throws error when adding two endpoints with the same name' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Example' - { Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'Example' } | Should -Throw -ExpectedMessage ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f 'Example') #'*already been defined*' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Example' + { Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'Example' } | Should -Throw -ExpectedMessage ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f 'Example') #'*already been defined*' } It 'Add two endpoints to listen on, one of SMTP and one of SMTPS' { @@ -352,7 +352,7 @@ Describe 'Add-PodeEndpoint' { It 'Add two endpoints to listen on, one of TCP and one of TCPS' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'TCP' -Name 'Tcp' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'TCP' -Name 'Tcp' Add-PodeEndpoint -Address 'pode.foo.com' -Port 443 -Protocol 'TCPS' -Name 'Tcps' $PodeContext.Server.Types | Should -Be 'TCP' @@ -371,7 +371,7 @@ Describe 'Add-PodeEndpoint' { } It 'Throws an error for not running as admin' { - Mock Test-PodeAdminPrivilege { return $false } + Mock Test-PodeBindToPrivilegedPort { throw $PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage} $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*' } @@ -392,9 +392,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns all Endpoints' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint $endpoints.Length | Should -Be 3 @@ -403,9 +403,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 3 endpoints by address - combination of ip/hostname' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address '127.0.0.1' $endpoints.Length | Should -Be 3 @@ -414,9 +414,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by hostname, and 3 by ip' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' $endpoints.Length | Should -Be 2 @@ -428,9 +428,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by hostname - old' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address 'pode.foo.com' $endpoints.Length | Should -Be 2 @@ -439,20 +439,20 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' - $endpoints = Get-PodeEndpoint -Port 80 + $endpoints = Get-PodeEndpoint -Port 36842 $endpoints.Length | Should -Be 2 } It 'Returns all endpoints by protocol' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Protocol Http $endpoints.Length | Should -Be 3 @@ -461,9 +461,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by name' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Admin' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'User' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' -Name 'Dev' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Admin' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'User' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' -Name 'Dev' $endpoints = Get-PodeEndpoint -Name Admin, User $endpoints.Length | Should -Be 2 @@ -472,18 +472,18 @@ Describe 'Get-PodeEndpoint' { It 'Returns 1 endpoint using everything' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Admin' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'User' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' -Name 'Dev' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Admin' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'User' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' -Name 'Dev' - $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' -Port 80 -Protocol Http -Name User + $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' -Port 36842 -Protocol Http -Name User $endpoints.Length | Should -Be 1 } It 'Returns endpoint set using wildcard' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '*' -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Address '*' -Port 36842 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address '*' $endpoints.Length | Should -Be 1 @@ -492,7 +492,7 @@ Describe 'Get-PodeEndpoint' { It 'Returns endpoint set using localhost' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address 'localhost' -Port 80 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'localhost' -Port 36842 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address 'localhost' $endpoints.Length | Should -Be 1 From 2622b6fa8081c8b5441bdc3d359415ce27ce0bb7 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 19 Apr 2025 10:20:48 -0700 Subject: [PATCH 89/93] fix check --- tests/unit/Context.Tests.ps1 | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index eae985e68..456ac409b 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -40,7 +40,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 8080 + $endpoint.Port | Should -Be 3684280 $endpoint.Name | Should -Not -Be ([string]::Empty) $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -57,7 +57,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 8080 + $endpoint.Port | Should -Be 3684280 $endpoint.Name | Should -Not -Be ([string]::Empty) $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -73,7 +73,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 8080 + $endpoint.Port | Should -Be 3684280 $endpoint.Name | Should -Be 'Example' $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -88,7 +88,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 8080 + $endpoint.Port | Should -Be 3684280 $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint.RawAddress | Should -Be 'foo.com:8080' @@ -103,7 +103,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -117,7 +117,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.2' $endpoint.RawAddress | Should -Be 'foo.com:80' @@ -132,7 +132,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 8080 + $endpoint.Port | Should -Be 3684280 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -146,7 +146,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 8080 + $endpoint.Port | Should -Be 3684280 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '0.0.0.0' $endpoint.RawAddress | Should -Be '0.0.0.0:8080' @@ -161,7 +161,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 8080 + $endpoint.Port | Should -Be 3684280 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -175,7 +175,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -189,7 +189,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -203,7 +203,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -217,7 +217,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.FriendlyName | Should -Be 'localhost' $endpoint.Address.ToString() | Should -Be '0.0.0.0' @@ -250,12 +250,12 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints[$ep1.Name] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints[$ep2.Name] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be 'pode.foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -270,13 +270,13 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Example1'] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.Name | Should -Be 'Example1' $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Example2'] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.Name | Should -Be 'Example2' $endpoint.HostName | Should -Be 'pode.foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -292,12 +292,12 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Http'] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Https'] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be 'pode.foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -312,7 +312,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = @($PodeContext.Server.Endpoints.Values)[0] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -360,7 +360,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Tcp'] - $endpoint.Port | Should -Be 80 + $endpoint.Port | Should -Be 36842 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' From 72271af138243438a2a18f4bbeb89c095898aa2d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 19 Apr 2025 10:21:56 -0700 Subject: [PATCH 90/93] revert some changes --- tests/unit/Context.Tests.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 456ac409b..6604ae989 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -40,7 +40,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 3684280 + $endpoint.Port | Should -Be 8080 $endpoint.Name | Should -Not -Be ([string]::Empty) $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -57,7 +57,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 3684280 + $endpoint.Port | Should -Be 8080 $endpoint.Name | Should -Not -Be ([string]::Empty) $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -73,7 +73,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 3684280 + $endpoint.Port | Should -Be 8080 $endpoint.Name | Should -Be 'Example' $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -88,7 +88,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 3684280 + $endpoint.Port | Should -Be 8080 $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint.RawAddress | Should -Be 'foo.com:8080' @@ -132,7 +132,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 3684280 + $endpoint.Port | Should -Be 8080 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -146,7 +146,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 3684280 + $endpoint.Port | Should -Be 8080 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '0.0.0.0' $endpoint.RawAddress | Should -Be '0.0.0.0:8080' @@ -161,7 +161,7 @@ Describe 'Add-PodeEndpoint' { $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 3684280 + $endpoint.Port | Should -Be 8080 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } From de50a2821c67765be23d7d8895c7c28945cc982d Mon Sep 17 00:00:00 2001 From: MDaneri Date: Sat, 19 Apr 2025 10:25:49 -0700 Subject: [PATCH 91/93] fix test --- tests/unit/Context.Tests.ps1 | 140 +++++++++++++++++------------------ 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 6604ae989..23e10eb1a 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -96,31 +96,31 @@ Describe 'Add-PodeEndpoint' { It 'Set both the Hostname address and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address 'foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'foo.com' -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set all the Hostname, ip and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.2' -Hostname 'foo.com' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.2' -Hostname 'foo.com' -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be 'foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.2' - $endpoint.RawAddress | Should -Be 'foo.com:80' + $endpoint.RawAddress | Should -Be 'foo.com:36880' } It 'Set just an IPv4 address' { @@ -168,60 +168,60 @@ Describe 'Add-PodeEndpoint' { It 'Set just a port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set just a port with colon' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set both IPv4 address and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set both IPv4 address and port for all' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '*' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address '*' -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.FriendlyName | Should -Be 'localhost' $endpoint.Address.ToString() | Should -Be '0.0.0.0' - $endpoint.RawAddress | Should -Be '0.0.0.0:80' + $endpoint.RawAddress | Should -Be '0.0.0.0:36880' } It 'Throws error for an invalid IPv4' { @@ -234,7 +234,7 @@ Describe 'Add-PodeEndpoint' { It 'Throws error for an invalid IPv4 address with port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '256.0.0.1' -Port 36842 -Protocol 'HTTP' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '256.0.0.1:80' ) #'*Failed to parse*' + { Add-PodeEndpoint -Address '256.0.0.1' -Port 36880 -Protocol 'HTTP' } | Should -Throw -ExpectedMessage ($PodeLocale.failedToParseAddressExceptionMessage -f '256.0.0.1:36880' ) #'*Failed to parse*' $PodeContext.Server.Types | Should -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 0 @@ -242,41 +242,41 @@ Describe 'Add-PodeEndpoint' { It 'Add two endpoints to listen on, of the same type' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - $ep1 = (Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -PassThru) - $ep2 = (Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -PassThru) + $ep1 = (Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' -PassThru) + $ep2 = (Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' -PassThru) $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints[$ep1.Name] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints[$ep2.Name] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be 'pode.foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Add two endpoints to listen on, with different names' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Example1' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'Example2' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' -Name 'Example1' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' -Name 'Example2' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Example1'] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.Name | Should -Be 'Example1' $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Example2'] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.Name | Should -Be 'Example2' $endpoint.HostName | Should -Be 'pode.foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' @@ -284,88 +284,88 @@ Describe 'Add-PodeEndpoint' { It 'Add two endpoints to listen on, one of HTTP and one of HTTPS' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Http' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTPS' -Name 'Https' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' -Name 'Http' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTPS' -Name 'Https' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Http'] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Https'] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be 'pode.foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Add two endpoints to listen on, but one added as they are the same' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = @($PodeContext.Server.Endpoints.Values)[0] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Allows adding two endpoints of different types' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'SMTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'SMTP' $PodeContext.Server.Endpoints.Count | Should -Be 2 } It 'Throws error when adding two endpoints with the same name' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Example' - { Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'Example' } | Should -Throw -ExpectedMessage ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f 'Example') #'*already been defined*' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' -Name 'Example' + { Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' -Name 'Example' } | Should -Throw -ExpectedMessage ($PodeLocale.endpointAlreadyDefinedExceptionMessage -f 'Example') #'*already been defined*' } It 'Add two endpoints to listen on, one of SMTP and one of SMTPS' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 25 -Protocol 'SMTP' -Name 'Smtp' - Add-PodeEndpoint -Address 'pode.mail.com' -Port 465 -Protocol 'SMTPS' -Name 'Smtps' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36825 -Protocol 'SMTP' -Name 'Smtp' + Add-PodeEndpoint -Address 'pode.mail.com' -Port 36465 -Protocol 'SMTPS' -Name 'Smtps' $PodeContext.Server.Types | Should -Be 'SMTP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Smtp'] - $endpoint.Port | Should -Be 25 + $endpoint.Port | Should -Be 36825 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Smtps'] - $endpoint.Port | Should -Be 465 + $endpoint.Port | Should -Be 36465 $endpoint.HostName | Should -Be 'pode.mail.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Add two endpoints to listen on, one of TCP and one of TCPS' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'TCP' -Name 'Tcp' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 443 -Protocol 'TCPS' -Name 'Tcps' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'TCP' -Name 'Tcp' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36443 -Protocol 'TCPS' -Name 'Tcps' $PodeContext.Server.Types | Should -Be 'TCP' $PodeContext.Server.Endpoints | Should -Not -Be $null $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Tcp'] - $endpoint.Port | Should -Be 36842 + $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be '' $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Tcps'] - $endpoint.Port | Should -Be 443 + $endpoint.Port | Should -Be 36443 $endpoint.HostName | Should -Be 'pode.foo.com' $endpoint.Address.ToString() | Should -Be '127.0.0.1' } @@ -392,9 +392,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns all Endpoints' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint $endpoints.Length | Should -Be 3 @@ -403,9 +403,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 3 endpoints by address - combination of ip/hostname' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address '127.0.0.1' $endpoints.Length | Should -Be 3 @@ -414,9 +414,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by hostname, and 3 by ip' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' $endpoints.Length | Should -Be 2 @@ -428,9 +428,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by hostname - old' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address 'pode.foo.com' $endpoints.Length | Should -Be 2 @@ -439,20 +439,20 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' - $endpoints = Get-PodeEndpoint -Port 36842 + $endpoints = Get-PodeEndpoint -Port 36880 $endpoints.Length | Should -Be 2 } It 'Returns all endpoints by protocol' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Protocol Http $endpoints.Length | Should -Be 3 @@ -461,9 +461,9 @@ Describe 'Get-PodeEndpoint' { It 'Returns 2 endpoints by name' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Admin' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'User' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' -Name 'Dev' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' -Name 'Admin' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' -Name 'User' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' -Name 'Dev' $endpoints = Get-PodeEndpoint -Name Admin, User $endpoints.Length | Should -Be 2 @@ -472,18 +472,18 @@ Describe 'Get-PodeEndpoint' { It 'Returns 1 endpoint using everything' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '127.0.0.1' -Port 36842 -Protocol 'HTTP' -Name 'Admin' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 36842 -Protocol 'HTTP' -Name 'User' - Add-PodeEndpoint -Address 'pode.foo.com' -Port 3684280 -Protocol 'HTTP' -Name 'Dev' + Add-PodeEndpoint -Address '127.0.0.1' -Port 36880 -Protocol 'HTTP' -Name 'Admin' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 36880 -Protocol 'HTTP' -Name 'User' + Add-PodeEndpoint -Address 'pode.foo.com' -Port 38080 -Protocol 'HTTP' -Name 'Dev' - $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' -Port 36842 -Protocol Http -Name User + $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' -Port 36880 -Protocol Http -Name User $endpoints.Length | Should -Be 1 } It 'Returns endpoint set using wildcard' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address '*' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address '*' -Port 36880 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address '*' $endpoints.Length | Should -Be 1 @@ -492,7 +492,7 @@ Describe 'Get-PodeEndpoint' { It 'Returns endpoint set using localhost' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } - Add-PodeEndpoint -Address 'localhost' -Port 36842 -Protocol 'HTTP' + Add-PodeEndpoint -Address 'localhost' -Port 36880 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address 'localhost' $endpoints.Length | Should -Be 1 From 6a525828902e8fabced18a04034db3d048c88769 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sun, 20 Apr 2025 07:57:10 -0700 Subject: [PATCH 92/93] remove write-verbose from a catch for powershell 5.1 compatibility --- src/Private/Helpers.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 0bc172183..a2ff5a461 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -4292,7 +4292,7 @@ function Test-PodeBindToPrivilegedPort { catch [System.Net.Sockets.SocketException] { switch ($_.Exception.SocketErrorCode) { 'AccessDenied' { - Write-Verbose "Access denied on $($IP):$p" + Write-Debug "Access denied on $($IP):$p" if ($ThrowError) { if (!$CheckAdmin) { return } throw ($PodeLocale.mustBeRunningWithAdminPrivilegesExceptionMessage) @@ -4304,18 +4304,18 @@ function Test-PodeBindToPrivilegedPort { } 'AddressAlreadyInUse' { - Write-Verbose "Port $p is already in use on $IP" + Write-Debug "Port $p is already in use on $IP" if ($Port.Count -gt 1) { continue } if ($ThrowError) { - throw ($PodeLocale.cannotBindPortInUseExceptionMessage -f $IP,$p) + throw ($PodeLocale.cannotBindPortInUseExceptionMessage -f $IP, $p) } return $false } default { - Write-Verbose "Unhandled socket error on $($IP):$p — $($_.Exception.SocketErrorCode)" + # Write-Debug "Unhandled socket error on $($IP):$p — $($_.Exception.SocketErrorCode)" if ($ThrowError) { throw $_ } @@ -4324,7 +4324,7 @@ function Test-PodeBindToPrivilegedPort { } } catch { - Write-Verbose "Unexpected error on $($IP):$p — $($_.Exception.Message)" + #Write-Debug "Unexpected error on $($IP):$p — $($_.Exception.Message)" if ($ThrowError) { throw $_ } @@ -4332,7 +4332,7 @@ function Test-PodeBindToPrivilegedPort { } } - Write-Verbose "All test ports on $IP were in use, but no privilege error detected" + Write-Debug "All test ports on $IP were in use, but no privilege error detected" return $false } From b300340e799888b62252c5177d920e029b3592df Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 21 Apr 2025 12:33:47 -0700 Subject: [PATCH 93/93] Update Context.Tests.ps1 --- tests/unit/Context.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 23e10eb1a..e13174a4a 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -110,7 +110,7 @@ Describe 'Add-PodeEndpoint' { It 'Set all the Hostname, ip and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - Add-PodeEndpoint -Address '127.0.0.2' -Hostname 'foo.com' -Port 36880 -Protocol 'HTTP' + Add-PodeEndpoint -Address '127.0.0.1' -Hostname 'foo.com' -Port 36880 -Protocol 'HTTP' $PodeContext.Server.Types | Should -Be 'HTTP' $PodeContext.Server.Endpoints | Should -Not -Be $null @@ -119,7 +119,7 @@ Describe 'Add-PodeEndpoint' { $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) $endpoint.Port | Should -Be 36880 $endpoint.HostName | Should -Be 'foo.com' - $endpoint.Address.ToString() | Should -Be '127.0.0.2' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint.RawAddress | Should -Be 'foo.com:36880' }