diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3a51c80f..f5f2ad39 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,7 +10,7 @@ jobs: - name: Install Prerequisites run: .\build\psf-prerequisites.ps1 shell: powershell - - name: Install Prerequisites + - name: Compile Help run: .\build\vsts-help.ps1 shell: powershell - name: Validate diff --git a/PSFramework/PSFramework.psd1 b/PSFramework/PSFramework.psd1 index dee5bde5..cf579cae 100644 --- a/PSFramework/PSFramework.psd1 +++ b/PSFramework/PSFramework.psd1 @@ -4,7 +4,7 @@ RootModule = 'PSFramework.psm1' # Version number of this module. - ModuleVersion = '1.13.419' + ModuleVersion = '1.13.425' # ID used to uniquely identify this module GUID = '8028b914-132b-431f-baa9-94a6952f21ff' diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll index c29b15d6..2dbbec8b 100644 Binary files a/PSFramework/bin/PSFramework.dll and b/PSFramework/bin/PSFramework.dll differ diff --git a/PSFramework/bin/PSFramework.pdb b/PSFramework/bin/PSFramework.pdb index d0578f0e..ca40ca17 100644 Binary files a/PSFramework/bin/PSFramework.pdb and b/PSFramework/bin/PSFramework.pdb differ diff --git a/PSFramework/bin/PSFramework.xml b/PSFramework/bin/PSFramework.xml index aa3b2049..aa297e96 100644 --- a/PSFramework/bin/PSFramework.xml +++ b/PSFramework/bin/PSFramework.xml @@ -4590,6 +4590,11 @@ Whether the console messages should be written with only one color, rather than respecting color tags + + + Whether the console messages should be written with multiple Write-Hosts, rather than one using ANSI sequences + + The format used in messages written on screen @@ -8478,6 +8483,11 @@ Class that contains the logic necessary to manage a unique runspace + + + The Generation of the Managed Runspace, determining the featureset used by it. + + The name of the runspace. @@ -8488,17 +8498,64 @@ The Guid of the running Runspace + + + The Code that will be used when running the runspace. + + Sets the script to execute in the runspace. Will NOT take immediate effect. Only after restarting the runspace will it be used. The scriptblock to execute + + + Creates a new Gen 1 runspace container with the basic information needed + + The name of the Runspace + The code using the runspace logic + + + + The initialization code of the runspace, executed in the global scope. + + + + + The execution code of the runspace, will be called repeatedly over the time of the runspace. + + + + + The finalization code of the runspace, will be called once when closing out the runspace. + + + + + Creates a new Gen 2 runspace container with the basic information needed + + The name of the Runspace + The initialization code of the runspace, executed in the global scope. + The execution code of the runspace, will be called repeatedly over the time of the runspace. + The finalization code of the runspace, will be called once when closing out the runspace. + + + + Initialize a runtime reference for the current managed runspace + + A Runtime reference, including the code for all three phases of a Gen 2 Managed Runspace + The state the runspace currently is in. + + + The last 50 errors that happened in the runspace + + Starts the Runspace. @@ -8525,26 +8582,24 @@ Signals the registered runspace has stopped execution - + - Creates a new runspace container with the basic information needed + Signals the registered runspace has failed badly and error logs should be checked. - The name of the Runspace - The code using the runspace logic Provides hosting for all registered runspaces - + - The number of seconds before a Stop command is interrupted and instead the runspace is gracelessly shut down. + The interval (in milliseonds) in which Runspace-Bound Values will be leaned up - + - The interval (in milliseonds) in which Runspace-Bound Values will be leaned up + The number of seconds before a Stop command is interrupted and instead the runspace is gracelessly shut down. @@ -8552,6 +8607,29 @@ The dictionary containing the definitive list of unique Runspace + + + Define the managed runspace or update its code as a Gen 1 Runspace + + The name of the managed runspace + The code implementing the managed runspace + Whether the runspace was created new (true) or merely updated (false) + + + + Define the managed runspace or update its code as a Gen 2 Runspace + + The name of the managed runspace + The Begin stage of the managed runspace + The Process stage of the managed runspace + The End stage of the managed runspace + Whether the runspace was created new (true) or merely updated (false) + + + + The Code to use for Gen 2 Managed Runspaces + + List of all runspace bound values in use @@ -8708,6 +8786,47 @@ The output result of the task The streams the task sent + + + Wrapper around the execution stages of a Generation 2 Managed Runspace + + + + + Begin phase of the Managed Runspace + + + + + Process phase of the Managed Runspace + + + + + End phase of the Managed Runspace + + + + + Access to the error queue of the managed runspace + + + + + The actual runspace container with the original task. + MAY BE MODIFIED after launch, should not be used for critical lifestate data. + + + + + Create a new Managed Runspace Runtime wrapper + + Begin phase of the Managed Runspace + Process phase of the Managed Runspace + End phase of the Managed Runspace + Access to the error queue of the managed runspace + The actual runspace container with the original task. + Contains the state a managed, unique runspace can be in. @@ -8728,6 +8847,11 @@ The runspace has followed its order to stop and is currently disabled + + + The runspace crashed and burned. It is also stopped without fracefully closing out, check the Errors property. + + An individual task executed in the runspace pool of its hosting RunspaceWrapper @@ -9638,6 +9762,11 @@ The task code to execute + + + Arguments to provide to the task code + + Whether the task is due and should be executed diff --git a/PSFramework/changelog.md b/PSFramework/changelog.md index 389818e6..e43a1a72 100644 --- a/PSFramework/changelog.md +++ b/PSFramework/changelog.md @@ -1,5 +1,14 @@ # CHANGELOG +## 1.13.425 (2026-01-07) + +- New: Configuration `PSFramework.Message.Style.OldColor` - reverts update to message printing, disabling use of ANSI codes for colors +- Upd: Register-PSFRunspace - Significantly reworked Managed Runspaces for greater ease of use. +- Upd: Register-PSFTaskEngineTask - Added `-ArgumentList` parameter to enable including data with Task +- Fix: Write-PSFMessage - Messages to host get jumbled when logging from parallel tasks (#707) +- Fix: Register-PSFRunspace - Closed Constrained Language Mode escape +- Fix: Register-PSFRunspace - Closed edge-case concurrency issue that was extremely unlikely to ever matter. + ## 1.13.419 (2025-11-24) - Fix: Get-PSFRunspaceLock - Fails to create new runspace lock. diff --git a/PSFramework/en-us/about_psf_runspace.help.txt b/PSFramework/en-us/about_psf_runspace.help.txt deleted file mode 100644 index 2d50068e..00000000 --- a/PSFramework/en-us/about_psf_runspace.help.txt +++ /dev/null @@ -1,20 +0,0 @@ -TOPIC - about_psf_runspace - -SHORT DESCRIPTION - Explains the PSFrameworks runspace component - -LONG DESCRIPTION - #-------------------------------------------------------------------------# - # Component Commands # - #-------------------------------------------------------------------------# - - - Get-PSFRunspace - - Register-PSFRunspace - - Start-PSFRunspace - - Stop-PSFRunspace - - - -KEYWORDS - psframework runspace \ No newline at end of file diff --git a/PSFramework/functions/message/Write-PSFHostColor.ps1 b/PSFramework/functions/message/Write-PSFHostColor.ps1 index ae551b24..350d4115 100644 --- a/PSFramework/functions/message/Write-PSFHostColor.ps1 +++ b/PSFramework/functions/message/Write-PSFHostColor.ps1 @@ -1,6 +1,5 @@ -function Write-PSFHostColor -{ -<# +function Write-PSFHostColor { + <# .SYNOPSIS Function that recognizes html-style tags to insert color into printed text. @@ -64,7 +63,7 @@ #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding(HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Write-PSFHostColor')] - Param ( + param ( [Parameter(ValueFromPipeline = $true)] [string[]] $String, @@ -78,52 +77,79 @@ [PSFramework.Message.MessageLevel] $Level ) - begin - { + begin { $em = [PSFramework.Message.MessageHost]::InfoColorEmphasis $sub = [PSFramework.Message.MessageHost]::InfoColorSubtle $max_info = [PSFramework.Message.MessageHost]::MaximumInformation $min_info = [PSFramework.Message.MessageHost]::MinimumInformation + + $useAnsi = -not [PSFramework.Message.MessageHost]::OldColor -and $host.UI.SupportsVirtualTerminal + + if ($useAnsi) { + $colorMap = @{ + Black = 30 + DarkRed = 31 + DarkGreen = 32 + DarkYellow = 33 + DarkBlue = 34 + DarkMagenta = 35 + DarkCyan = 36 + Gray = 37 + DarkGray = 90 + Red = 91 + Green = 92 + Yellow = 93 + Blue = 94 + Magenta = 95 + Cyan = 96 + White = 97 + } + $escape = [char]27 + '[' + $defaultCode = $colorMap["$DefaultColor"] + } } - process - { - if ($Level) - { + process { + if ($Level) { if (($max_info -lt $Level) -or ($min_info -gt $Level)) { return } } - foreach ($line in $String) - { - foreach ($row in $line.Split("`n")) #.Split([environment]::NewLine)) - { - if ($row -notlike '***') { Microsoft.PowerShell.Utility\Write-Host -Object $row -ForegroundColor $DefaultColor -NoNewline:$NoNewLine } - else - { - $row = $row -replace '', "" -replace '', "" - $match = ($row | Select-String '(.*?)' -AllMatches).Matches - $index = 0 - $count = 0 + foreach ($line in $String) { + foreach ($row in $line.Split("`n")) { + #.Split([environment]::NewLine)) + if ($row -notlike '***') { + Microsoft.PowerShell.Utility\Write-Host -Object $row -ForegroundColor $DefaultColor -NoNewline:$NoNewLine + continue + } + + $row = $row -replace '', "" -replace '', "" + $match = ($row | Select-String '(.*?)' -AllMatches).Matches + if ($useAnsi) { + foreach ($entry in $match) { + $row = $row -replace $entry.Groups[0].Value, "$($escape)$($colorMap[$entry.Groups[1].Value])m$($entry.Groups[2].Value)$($escape)$($defaultCode)m" + } + Microsoft.PowerShell.Utility\Write-Host -Object $row -ForegroundColor $DefaultColor + continue + } + + $index = 0 + $count = 0 - while ($count -le $match.Count) - { - if ($count -lt $Match.Count) - { - Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index, ($match[$count].Index - $Index)) -ForegroundColor $DefaultColor -NoNewline - try { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $match[$count].Groups[1].Value -NoNewline -ErrorAction Stop } - catch { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $DefaultColor -NoNewline -ErrorAction Stop } + while ($count -le $match.Count) { + if ($count -lt $Match.Count) { + Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index, ($match[$count].Index - $Index)) -ForegroundColor $DefaultColor -NoNewline + try { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $match[$count].Groups[1].Value -NoNewline -ErrorAction Stop } + catch { Microsoft.PowerShell.Utility\Write-Host -Object $match[$count].Groups[2].Value -ForegroundColor $DefaultColor -NoNewline -ErrorAction Stop } - $index = $match[$count].Index + $match[$count].Length - $count++ - } - else - { - Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index) -ForegroundColor $DefaultColor -NoNewline:$NoNewLine - $count++ - } + $index = $match[$count].Index + $match[$count].Length + $count++ + } + else { + Microsoft.PowerShell.Utility\Write-Host -Object $row.SubString($index) -ForegroundColor $DefaultColor -NoNewline:$NoNewLine + $count++ } } } } } -} +} \ No newline at end of file diff --git a/PSFramework/functions/runspace/Get-PSFRunspace.ps1 b/PSFramework/functions/runspace/Get-PSFRunspace.ps1 index 70e19de1..b16a3798 100644 --- a/PSFramework/functions/runspace/Get-PSFRunspace.ps1 +++ b/PSFramework/functions/runspace/Get-PSFRunspace.ps1 @@ -29,6 +29,6 @@ process { - [PSFramework.Runspace.RunspaceHost]::Runspaces.Values | Where-Object Name -Like $Name + ([PSFramework.Runspace.RunspaceHost]::Runspaces.Values) | Where-Object Name -Like $Name } } \ No newline at end of file diff --git a/PSFramework/functions/runspace/Register-PSFRunspace.ps1 b/PSFramework/functions/runspace/Register-PSFRunspace.ps1 index 81784bf4..9f7670e1 100644 --- a/PSFramework/functions/runspace/Register-PSFRunspace.ps1 +++ b/PSFramework/functions/runspace/Register-PSFRunspace.ps1 @@ -9,8 +9,12 @@ This is different from most runspace solutions, in that it is designed for permanent background tasks that need to be done. It guarantees a single copy of the task to run within the powershell process, even when running the same module in many runspaces in parallel. - The scriptblock must be built with some rules in mind, for details on using this system run: - Get-Help about_psf_runspace + There are two Generations of the Managed Runspace system available: + Gen 1: -ScriptBlock | Full control over the execution, but some integrations are required for the whole thing to work. + Gen 2: -Begin/-Process/-End | Simple execution that is easy to use, but surrenders some control over the process. + + The full documentation can be found online: https://psframework.org + By default, using the Generation 2 implementation is recommended, the third shows how to use it. Updating: If this function is called multiple times, targeting the same name, it will update the scriptblock. @@ -18,15 +22,34 @@ - If that scriptblock is different from the previous ones, it will be registered, but will not be executed right away! Only after stopping and starting the runspace will it operate under the new scriptblock. - .PARAMETER ScriptBlock - The scriptblock to run in a dedicated runspace. - .PARAMETER Name The name to register the scriptblock under. + .PARAMETER ScriptBlock + The scriptblock to run in a dedicated runspace. + Scriptblock must be trusted and not in Constrained Language Mode. + + .PARAMETER Begin + The startup phase in a Generation 2 Managed Runspace. + Will be executed once in the managed runspace and will run in the global scope. + Scriptblock must be trusted and not in Constrained Language Mode. + + .PARAMETER Process + The main processing phase in a Generation 2 Managed Runspace. + Will be executed repeatedly until the Managed Runspace is stopped. + There is a 250ms wait inbetween executions, to prevent CPU overload, additional waiting can be performed within the code. + To stop the entire Managed Runspace from within your code, call "break" - this will not prevent the End phase from executing if specified. + + .PARAMETER End + The end phase in a Generation 2 Managed Runspace. + Will be executed once at the end when stopping the Managed Runspace + .PARAMETER NoMessage Setting this will prevent messages be written to the message / logging system. This is designed to make the PSFramework not flood the log on each import. + + .PARAMETER Start + Automatically start the runspace after registering it. .EXAMPLE PS C:\> Register-PSFRunspace -ScriptBlock $scriptBlock -Name 'mymodule.maintenance' @@ -40,30 +63,63 @@ Registers the script defined in $scriptBlock under the name 'mymodule.maintenance' Then it starts the runspace, running the registered $scriptBlock + + .EXAMPLE + PS C:\> $code = { Remove-Item -Path "$env:TEMP\MyModule-*" -Force -Recurse; Start-Sleep -Seconds 30 } + PS C:\> Register-PSFRunspace -Name 'mymodule.tempcleaner' -Process $code -Start + + Creates a background runspace that can never exist more than once in the current process. + It will clean all items under $env:TEMP that start with "MyModule-" every 30 seconds (plus 250ms, which are added by the system). #> - [CmdletBinding(PositionalBinding = $false, HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFRunspace')] + [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = 'Gen2', HelpUri = 'https://psframework.org/documentation/commands/PSFramework/Register-PSFRunspace')] param ( [Parameter(Mandatory = $true)] + [String] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Gen1')] + [PSFramework.Validation.PsfValidateLanguageMode()] [Scriptblock] $ScriptBlock, + + [Parameter(ParameterSetName = 'Gen2')] + [PSFramework.Validation.PsfValidateLanguageMode()] + [Scriptblock] + $Begin, - [Parameter(Mandatory = $true)] - [String] - $Name, + [Parameter(Mandatory = $true, ParameterSetName = 'Gen2')] + [Scriptblock] + $Process, + [Parameter(ParameterSetName = 'Gen2')] + [Scriptblock] + $End, + + [switch] + $NoMessage, + [switch] - $NoMessage + $Start ) + + switch ($PSCmdlet.ParameterSetName) { + Gen1 { $wasNew = [PSFramework.Runspace.RunspaceHost]::SetManagedRunspace($Name, $ScriptBlock) } + Gen2 { $wasNew = [PSFramework.Runspace.RunspaceHost]::SetManagedRunspace($Name, $Begin, $Process, $End) } + } - if ([PSFramework.Runspace.RunspaceHost]::Runspaces.ContainsKey($Name)) - { - if (-not $NoMessage) { Write-PSFMessage -Level Verbose -String 'Register-PSFRunspace.Runspace.Updating' -StringValues $Name -Target $Name -Tag 'runspace', 'register' } - [PSFramework.Runspace.RunspaceHost]::Runspaces[$Name].SetScript($ScriptBlock) + if (-not $NoMessage) { + if (-not $wasNew) + { + Write-PSFMessage -Level Verbose -String 'Register-PSFRunspace.Runspace.Updating' -StringValues $Name -Target $Name -Tag 'runspace', 'register' + } + else + { + Write-PSFMessage -Level Verbose -String 'Register-PSFRunspace.Runspace.Creating' -StringValues $Name -Target $Name -Tag 'runspace', 'register' + } } - else - { - if (-not $NoMessage) { Write-PSFMessage -Level Verbose -String 'Register-PSFRunspace.Runspace.Creating' -StringValues $Name -Target $Name -Tag 'runspace', 'register' } - [PSFramework.Runspace.RunspaceHost]::Runspaces[$Name] = New-Object PSFramework.Runspace.RunspaceContainer($Name, $ScriptBlock) + + if ($Start) { + Start-PSFRunspace -Name $Name -NoMessage:$NoMessage } } \ No newline at end of file diff --git a/PSFramework/functions/taskengine/Get-PSFTaskEngineTask.ps1 b/PSFramework/functions/taskengine/Get-PSFTaskEngineTask.ps1 index 94651dd8..7d3d281d 100644 --- a/PSFramework/functions/taskengine/Get-PSFTaskEngineTask.ps1 +++ b/PSFramework/functions/taskengine/Get-PSFTaskEngineTask.ps1 @@ -30,6 +30,6 @@ process { - [PSFramework.TaskEngine.TaskHost]::Tasks.Values | Where-Object Name -Like $Name + $([PSFramework.TaskEngine.TaskHost]::Tasks.Values | Where-Object Name -Like $Name) } } \ No newline at end of file diff --git a/PSFramework/functions/taskengine/Register-PSFTaskEngineTask.ps1 b/PSFramework/functions/taskengine/Register-PSFTaskEngineTask.ps1 index 13b7c6a6..f05bdfcd 100644 --- a/PSFramework/functions/taskengine/Register-PSFTaskEngineTask.ps1 +++ b/PSFramework/functions/taskengine/Register-PSFTaskEngineTask.ps1 @@ -41,6 +41,9 @@ .PARAMETER Priority How important is this task? If multiple tasks are due at the same maintenance cycle, the more critical one will be processed first. + + .PARAMETER ArgumentList + Arguments to provide when executing the task. .PARAMETER ResetTask If the task already exists, it will be reset by setting this parameter (this switch is ignored when creating new tasks). @@ -90,6 +93,10 @@ [PSFramework.TaskEngine.Priority] $Priority = "Medium", + + [AllowNull()] + [object] + $ArgumentList, [switch] $ResetTask, @@ -115,6 +122,7 @@ } if (Test-PSFParameterBinding -ParameterName Delay) { $task.Delay = $Delay } if (Test-PSFParameterBinding -ParameterName Priority) { $task.Priority = $Priority } + if (Test-PSFParameterBinding -ParameterName ArgumentList) { $task.ArgumentList = $ArgumentList } if ($ResetTask) { @@ -135,6 +143,7 @@ if (Test-PSFParameterBinding -ParameterName Once) { $task.Once = $true } if (Test-PSFParameterBinding -ParameterName Interval) { $task.Interval = $Interval } if (Test-PSFParameterBinding -ParameterName Delay) { $task.Delay = $Delay } + if (Test-PSFParameterBinding -ParameterName ArgumentList) { $task.ArgumentList = $ArgumentList } $task.Priority = $Priority $task.Registered = Get-Date [PSFramework.TaskEngine.TaskHost]::Tasks[$Name] = $task diff --git a/PSFramework/internal/configurations/message.ps1 b/PSFramework/internal/configurations/message.ps1 index 6457426e..9c020983 100644 --- a/PSFramework/internal/configurations/message.ps1 +++ b/PSFramework/internal/configurations/message.ps1 @@ -19,6 +19,7 @@ Set-PSFConfig -Module PSFramework -Name 'Message.Style.Timestamp' -Value $true - Set-PSFConfig -Module PSFramework -Name 'Message.Style.Target' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageTarget = $_ } -Description "Controls how messages are displayed. Include the message target (if present) in verbose message output." Set-PSFConfig -Module PSFramework -Name 'Message.Style.Level' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessageLevel = $_ } -Description "Controls how messages are displayed. Enables level display, including its level in each message." Set-PSFConfig -Module PSFramework -Name 'Message.Style.NoColor' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::NoColor = $_ } -Description "Controls how messages are displayed. Disables colorization of the messages shown on screen. This prevents messages being broken into multiple lines on some agent system such as the universal console or Azure DevOps logs." +Set-PSFConfig -Module PSFramework -Name 'Message.Style.OldColor' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::OldColor = $_ } -Description "Controls how messages are displayed. Disable colorization with ANSI sequences, forcing legacy colorization of the messages shown on screen, splitting messages in separate parcels on screen. Will display fine on most computers, but costs more performance." Set-PSFConfig -Module PSFramework -Name 'Message.Style.TimeFormat' -Value 'HH:mm:ss' -Initialize -Validation "string" -Handler { [PSFramework.Message.MessageHost]::TimeFormat = $_ } -Description "Controls how messages are displayed. The format used in timestamps for messages written on screen" Set-PSFConfig -Module PSFramework -Name 'Message.Style.Prefix' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::EnableMessagePrefix = $_ } -Description "Controls how messages are displayed. Enables message prefix display, including a prefix in each message." diff --git a/PSFramework/internal/scripts/postimport.ps1 b/PSFramework/internal/scripts/postimport.ps1 index 8eff3004..6c1f6855 100644 --- a/PSFramework/internal/scripts/postimport.ps1 +++ b/PSFramework/internal/scripts/postimport.ps1 @@ -26,6 +26,9 @@ foreach ($file in (Get-ChildItem -Path "$($moduleRoot)\internal\configurations\* # Import configuration settings from registry "$($moduleRoot)\internal\scripts\loadConfigurationPersisted.ps1" +# Launch the Managed Runspace System +"$($moduleRoot)\internal\scripts\runspaceManagedGen2.ps1" + # Load each logging provider foreach ($file in (Get-ChildItem -Path "$($moduleRoot)\internal\loggingProviders\*.ps1")) { diff --git a/PSFramework/internal/scripts/runspaceManagedGen2.ps1 b/PSFramework/internal/scripts/runspaceManagedGen2.ps1 new file mode 100644 index 00000000..5bf54022 --- /dev/null +++ b/PSFramework/internal/scripts/runspaceManagedGen2.ps1 @@ -0,0 +1,50 @@ +[PSFramework.Runspace.RunspaceHost]::ManagedRunspaceCodeGen2 = { + param ($__PSF_Runspace) + + Set-Variable -Name __PSF_Runtime -Value $__PSF_Runspace.GetRuntime() -Option Constant + Set-Variable -Name __PSF_Runspace -Option Constant + + $ErrorActionPreference = 'Stop' + trap { + if ($_.Exception.ErrorRecord) { + $null = $__PSF_Runtime.Errors.TryAdd($_.Exception.ErrorRecord) + } + else { + $null = $__PSF_Runtime.Errors.TryAdd($_) + } + $__PSF_Runtime.Workload.SignalFailed() + throw + } + + # Execute the Begin Stage + if ($__PSF_Runtime.Begin) { + $__PSF_Runtime.Begin.InvokeEx($true, $false, $false) + } + + while ($true) { + if ($__PSF_Runtime.Workload.State -notlike 'Running') { + break + } + + try { + $null = $__PSF_Runtime.Process.InvokeEx($false, $false, $false) + } + catch { + if ($_.Exception.ErrorRecord) { + $null = $__PSF_Runtime.Errors.TryAdd($_.Exception.ErrorRecord) + } + else { + $null = $__PSF_Runtime.Errors.TryAdd($_) + } + } + + Start-Sleep -Milliseconds 250 + } + + # Execute the End Stage + if ($__PSF_Runtime.End) { + $__PSF_Runtime.End.InvokeEx($false, $false, $false) + } + + $__PSF_Runtime.Workload.SignalStopped() +} \ No newline at end of file diff --git a/PSFramework/internal/scripts/taskEngine.ps1 b/PSFramework/internal/scripts/taskEngine.ps1 index c5052061..1e1139f0 100644 --- a/PSFramework/internal/scripts/taskEngine.ps1 +++ b/PSFramework/internal/scripts/taskEngine.ps1 @@ -1,36 +1,31 @@ $scriptBlock = { $script:___ScriptName = 'psframework.taskengine' - try - { + try { #region Main Execution - while ($true) - { + while ($true) { # This portion is critical to gracefully closing the script - if ([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].State -notlike "Running") - { + if ([PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].State -notlike "Running") { break } $task = $null $tasksDone = @() - while ($task = [PSFramework.TaskEngine.TaskHost]::GetNextTask($tasksDone)) - { + while ($task = [PSFramework.TaskEngine.TaskHost]::GetNextTask($tasksDone)) { $task.State = 'Running' - try - { + try { [PSFramework.Utility.UtilityHost]::ImportScriptBlock($task.ScriptBlock) - $task.ScriptBlock.Invoke() + if ($null -ne $task.ArgumentList) { $task.ScriptBlock.Invoke($task.ArgumentList) } + else { $task.ScriptBlock.Invoke() } $task.State = 'Pending' } - catch - { + catch { $task.State = 'Error' $task.LastError = $_ Write-PSFMessage -EnableException $false -Level Warning -Message "[Maintenance] Task '$($task.Name)' failed to execute" -ErrorRecord $_ -FunctionName "task:TaskEngine" -Target $task -ModuleName PSFramework } $task.LastExecution = Get-Date - if (-not $task.Pending -and ($task.Status -eq "Pending")) { $task.Status = 'Completed' } + if (-not $task.Pending -and ($task.State -eq "Pending")) { $task.State = 'Completed' } $tasksDone += $task.Name } @@ -41,9 +36,10 @@ } #endregion Main Execution } - catch { } - finally - { + catch { + Write-PSFMessage -Level Warning -Message "[Maintenance] Unhandled Error executing Task Engine" -ErrorRecord $_ + } + finally { [PSFramework.Runspace.RunspaceHost]::Runspaces[$___ScriptName].SignalStopped() } } diff --git a/PSFramework/xml/PSFramework.Format.ps1xml b/PSFramework/xml/PSFramework.Format.ps1xml index 106ccc9e..78e473b1 100644 --- a/PSFramework/xml/PSFramework.Format.ps1xml +++ b/PSFramework/xml/PSFramework.Format.ps1xml @@ -714,6 +714,45 @@ + + + PSFramework.Runspace.RunspaceContainer + + PSFramework.Runspace.RunspaceContainer + + + + + + + + + + + + + + + + + $_.Generation + + + + Name + + + RunspaceGuid + + + State + + + + + + + PSFramework.Runspace.RunspaceResult diff --git a/build/psf-prerequisites.ps1 b/build/psf-prerequisites.ps1 index 572bb540..36dd83b3 100644 --- a/build/psf-prerequisites.ps1 +++ b/build/psf-prerequisites.ps1 @@ -1,4 +1,4 @@ -Invoke-WebRequest https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1 | Invoke-Expression +Invoke-WebRequest https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1 -UseBasicParsing | Invoke-Expression Install-PSFPowerShellGet Install-PSFModule -Name Pester,PSScriptAnalyzer, PlatyPS, PSModuleDevelopment \ No newline at end of file diff --git a/library/PSFramework/Message/MessageHost.cs b/library/PSFramework/Message/MessageHost.cs index 3cc81508..7889fa3d 100644 --- a/library/PSFramework/Message/MessageHost.cs +++ b/library/PSFramework/Message/MessageHost.cs @@ -111,6 +111,11 @@ public static class MessageHost /// public static bool NoColor = false; + /// + /// Whether the console messages should be written with multiple Write-Hosts, rather than one using ANSI sequences + /// + public static bool OldColor = false; + /// /// The format used in messages written on screen /// diff --git a/library/PSFramework/PSFramework.csproj b/library/PSFramework/PSFramework.csproj index 0e58aaf3..fa046588 100644 --- a/library/PSFramework/PSFramework.csproj +++ b/library/PSFramework/PSFramework.csproj @@ -220,6 +220,7 @@ + diff --git a/library/PSFramework/Runspace/RunspaceContainer.cs b/library/PSFramework/Runspace/RunspaceContainer.cs index 6a70dd98..8b419a6e 100644 --- a/library/PSFramework/Runspace/RunspaceContainer.cs +++ b/library/PSFramework/Runspace/RunspaceContainer.cs @@ -1,4 +1,5 @@ -using System; +using PSFramework.Utility; +using System; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Threading; @@ -11,10 +12,13 @@ namespace PSFramework.Runspace /// public class RunspaceContainer { - private ScriptBlock Script; - private PowerShell Runspace; + /// + /// The Generation of the Managed Runspace, determining the featureset used by it. + /// + public int Generation { get; internal set; } + /// /// The name of the runspace. /// @@ -28,6 +32,12 @@ public Guid RunspaceGuid get { return Runspace.Runspace.InstanceId; } } + #region Generation 1 + /// + /// The Code that will be used when running the runspace. + /// + public ScriptBlock Script { get; private set; } + /// /// Sets the script to execute in the runspace. Will NOT take immediate effect. Only after restarting the runspace will it be used. /// @@ -37,6 +47,61 @@ public void SetScript(ScriptBlock Script) this.Script = Script; } + /// + /// Creates a new Gen 1 runspace container with the basic information needed + /// + /// The name of the Runspace + /// The code using the runspace logic + public RunspaceContainer(string Name, ScriptBlock Script) + { + this.Name = Name.ToLower(); + this.Script = Script; + Generation = 1; + } + #endregion Generation 1 + + #region Generation 2 + /// + /// The initialization code of the runspace, executed in the global scope. + /// + public ScriptBlock Begin { get; internal set; } + /// + /// The execution code of the runspace, will be called repeatedly over the time of the runspace. + /// + public ScriptBlock Process { get; internal set; } + /// + /// The finalization code of the runspace, will be called once when closing out the runspace. + /// + public ScriptBlock End { get; internal set; } + + /// + /// Creates a new Gen 2 runspace container with the basic information needed + /// + /// The name of the Runspace + /// The initialization code of the runspace, executed in the global scope. + /// The execution code of the runspace, will be called repeatedly over the time of the runspace. + /// The finalization code of the runspace, will be called once when closing out the runspace. + public RunspaceContainer(string Name, ScriptBlock Begin, ScriptBlock Process, ScriptBlock End) + { + this.Name = Name.ToLower(); + this.Begin = Begin; + this.Process = Process; + this.End = End; + Script = RunspaceHost.ManagedRunspaceCodeGen2; + Generation = 2; + } + + /// + /// Initialize a runtime reference for the current managed runspace + /// + /// A Runtime reference, including the code for all three phases of a Gen 2 Managed Runspace + public RunspaceRuntime GetRuntime() + { + return new RunspaceRuntime(Begin, Process, End, Errors, this); + } + #endregion Generation 2 + + #region Runtime /// /// The state the runspace currently is in. /// @@ -46,12 +111,17 @@ public PsfRunspaceState State } private PsfRunspaceState _State = PsfRunspaceState.Stopped; + /// + /// The last 50 errors that happened in the runspace + /// + public LimitedConcurrentQueue Errors = new LimitedConcurrentQueue(50); + /// /// Starts the Runspace. /// public void Start() { - if ((Runspace != null) && (State == PsfRunspaceState.Stopped)) + if ((Runspace != null) && ((State == PsfRunspaceState.Stopped) || (State == PsfRunspaceState.Failed))) { Kill(); } @@ -61,10 +131,13 @@ public void Start() Runspace = PowerShell.Create(); try { SetName(Runspace.Runspace); } catch { } - Runspace.AddScript(Script.ToString()); + Runspace.AddScript(Script.ToString()).AddArgument(this); _State = PsfRunspaceState.Running; try { Runspace.BeginInvoke(); } - catch { _State = PsfRunspaceState.Stopped; } + catch (Exception e) { + Errors.TryAdd(new ErrorRecord(e, "RunspaceEngineFail", ErrorCategory.OpenError, null)); + _State = PsfRunspaceState.Failed; + } } } @@ -118,18 +191,17 @@ public void Kill() /// public void SignalStopped() { - _State = PsfRunspaceState.Stopped; + if (_State != PsfRunspaceState.Failed) + _State = PsfRunspaceState.Stopped; } /// - /// Creates a new runspace container with the basic information needed + /// Signals the registered runspace has failed badly and error logs should be checked. /// - /// The name of the Runspace - /// The code using the runspace logic - public RunspaceContainer(string Name, ScriptBlock Script) + public void SignalFailed() { - this.Name = Name.ToLower(); - this.Script = Script; + _State = PsfRunspaceState.Failed; } + #endregion Runtime } } diff --git a/library/PSFramework/Runspace/RunspaceHost.cs b/library/PSFramework/Runspace/RunspaceHost.cs index 0b805830..ae7b125c 100644 --- a/library/PSFramework/Runspace/RunspaceHost.cs +++ b/library/PSFramework/Runspace/RunspaceHost.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Management.Automation; using System.Timers; namespace PSFramework.Runspace @@ -10,11 +11,6 @@ namespace PSFramework.Runspace /// public static class RunspaceHost { - /// - /// The number of seconds before a Stop command is interrupted and instead the runspace is gracelessly shut down. - /// - public static int StopTimeoutSeconds = 30; - /// /// The interval (in milliseonds) in which Runspace-Bound Values will be leaned up /// @@ -30,11 +26,87 @@ public static int RbvCleanupInterval } private static int _RbvCleanupInterval = 900000; + #region Managed Runspaces + /// + /// The number of seconds before a Stop command is interrupted and instead the runspace is gracelessly shut down. + /// + public static int StopTimeoutSeconds = 30; + /// /// The dictionary containing the definitive list of unique Runspace /// public static ConcurrentDictionary Runspaces = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase); + private static object _MRLock = 42; + /// + /// Define the managed runspace or update its code as a Gen 1 Runspace + /// + /// The name of the managed runspace + /// The code implementing the managed runspace + /// Whether the runspace was created new (true) or merely updated (false) + public static bool SetManagedRunspace(string Name, ScriptBlock Code) + { + bool isNew = false; + lock (_MRLock) + { + isNew = !Runspaces.ContainsKey(Name); + + if (isNew) + Runspaces[Name] = new RunspaceContainer(Name, Code); + else + { + Runspaces[Name].SetScript(Code); + Runspaces[Name].Generation = 1; + } + } + return isNew; + } + + /// + /// Define the managed runspace or update its code as a Gen 2 Runspace + /// + /// The name of the managed runspace + /// The Begin stage of the managed runspace + /// The Process stage of the managed runspace + /// The End stage of the managed runspace + /// Whether the runspace was created new (true) or merely updated (false) + public static bool SetManagedRunspace(string Name, ScriptBlock Begin, ScriptBlock Process, ScriptBlock End) + { + bool isNew = false; + lock (_MRLock) + { + isNew = !Runspaces.ContainsKey(Name); + + if (isNew) + Runspaces[Name] = new RunspaceContainer(Name, Begin, Process, End); + else + { + Runspaces[Name].SetScript(ManagedRunspaceCodeGen2); + Runspaces[Name].Begin = Begin; + Runspaces[Name].Process = Process; + Runspaces[Name].End = End; + Runspaces[Name].Generation = 2; + } + } + return isNew; + } + + /// + /// The Code to use for Gen 2 Managed Runspaces + /// + public static ScriptBlock ManagedRunspaceCodeGen2 + { + get => _ManagedRunspaceCodeGen2; + set + { + if (_ManagedRunspaceCodeGen2 != null) + return; + _ManagedRunspaceCodeGen2 = value; + } + } + private static ScriptBlock _ManagedRunspaceCodeGen2; + #endregion Managed Runspaces + #region Runspace Bound Values /// /// List of all runspace bound values in use diff --git a/library/PSFramework/Runspace/RunspaceRuntime.cs b/library/PSFramework/Runspace/RunspaceRuntime.cs new file mode 100644 index 00000000..96474b6c --- /dev/null +++ b/library/PSFramework/Runspace/RunspaceRuntime.cs @@ -0,0 +1,58 @@ +using PSFramework.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace PSFramework.Runspace +{ + /// + /// Wrapper around the execution stages of a Generation 2 Managed Runspace + /// + public class RunspaceRuntime + { + /// + /// Begin phase of the Managed Runspace + /// + public readonly PsfScriptBlock Begin; + /// + /// Process phase of the Managed Runspace + /// + public readonly PsfScriptBlock Process; + /// + /// End phase of the Managed Runspace + /// + public readonly PsfScriptBlock End; + /// + /// Access to the error queue of the managed runspace + /// + public readonly LimitedConcurrentQueue Errors; + /// + /// The actual runspace container with the original task. + /// MAY BE MODIFIED after launch, should not be used for critical lifestate data. + /// + public readonly RunspaceContainer Workload; + + /// + /// Create a new Managed Runspace Runtime wrapper + /// + /// Begin phase of the Managed Runspace + /// Process phase of the Managed Runspace + /// End phase of the Managed Runspace + /// Access to the error queue of the managed runspace + /// The actual runspace container with the original task. + public RunspaceRuntime(ScriptBlock Begin, ScriptBlock Process, ScriptBlock End, LimitedConcurrentQueue Errors, RunspaceContainer Workload) + { + if (Begin != null) + this.Begin = ((PsfScriptBlock)Begin).ToGlobal(); + if (Process != null) + this.Process = ((PsfScriptBlock)Process).ToGlobal(); + if (End != null) + this.End = ((PsfScriptBlock)End).ToGlobal(); + this.Errors = Errors; + this.Workload = Workload; + } + } +} diff --git a/library/PSFramework/Runspace/RunspaceState.cs b/library/PSFramework/Runspace/RunspaceState.cs index 5a2bc680..07244c5f 100644 --- a/library/PSFramework/Runspace/RunspaceState.cs +++ b/library/PSFramework/Runspace/RunspaceState.cs @@ -19,5 +19,10 @@ public enum PsfRunspaceState /// The runspace has followed its order to stop and is currently disabled /// Stopped = 3, + + /// + /// The runspace crashed and burned. It is also stopped without fracefully closing out, check the Errors property. + /// + Failed = 4 } } diff --git a/library/PSFramework/TaskEngine/PsfTask.cs b/library/PSFramework/TaskEngine/PsfTask.cs index d4924d85..b66b6c31 100644 --- a/library/PSFramework/TaskEngine/PsfTask.cs +++ b/library/PSFramework/TaskEngine/PsfTask.cs @@ -82,6 +82,11 @@ public DateTime NextExecution /// public ScriptBlock ScriptBlock; + /// + /// Arguments to provide to the task code + /// + public object ArgumentList; + /// /// Whether the task is due and should be executed ///