diff --git a/.gitignore b/.gitignore
index beab25e3..ed24bb6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,7 @@ PSFramework/PSFramework.psproj
TestResults/*
# ignore the publishing Directory
-publish/*
\ No newline at end of file
+publish/*
+
+# Ignore local test files (should be migrated to pester tests anyway)
+localTests/*
\ No newline at end of file
diff --git a/PSFramework/PSFramework.psd1 b/PSFramework/PSFramework.psd1
index 8c830480..b4dcfd12 100644
--- a/PSFramework/PSFramework.psd1
+++ b/PSFramework/PSFramework.psd1
@@ -4,7 +4,7 @@
RootModule = 'PSFramework.psm1'
# Version number of this module.
- ModuleVersion = '1.14.441'
+ ModuleVersion = '1.14.449'
# ID used to uniquely identify this module
GUID = '8028b914-132b-431f-baa9-94a6952f21ff'
@@ -144,6 +144,7 @@
'Remove-PSFTempItem'
'Remove-PSFTeppCompletion'
'Reset-PSFConfig'
+ 'Reset-PSFRsAgentInactivity'
'Resolve-PSFDefaultParameterValue'
'Resolve-PSFItem'
'Resolve-PSFPath'
diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll
index 5caef99c..23cef641 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 e130183a..f2803383 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 dbdc7d6c..d27b9429 100644
--- a/PSFramework/bin/PSFramework.xml
+++ b/PSFramework/bin/PSFramework.xml
@@ -140,6 +140,13 @@
The key to check
Whether the key is in the cache
+
+
+ Verifies whether the key exists in the base hashtable.
+
+ The key to check
+ Whether the key is in the cache
+
The number of non-expired items in the cache
@@ -1335,6 +1342,11 @@
The cmdlet object to use for writing errors.
+
+
+ The log message should have the script-stacktrace of the error record if specified
+
+
The start time of the cmdlet
@@ -4188,6 +4200,12 @@
The live powershell callstack
+
+
+ Initialize a callstack from a ScriptCallStack as provided by an ErrorRecord
+
+ The ScriptCallStack provided by an ErrorRecord
+
A single entry within the callstack
@@ -4544,7 +4562,7 @@
The computer the error was written on
The error entry, so it may be included in the message generated
-
+
Write a new entry to the log
@@ -4564,9 +4582,10 @@
The callstack at the moment the message was written.
The name of the user under which the code being executed
An associated error record
+ Whether to use the error record for logging the callstack
The entry that is being written
-
+
Write a new entry to the log
@@ -4588,6 +4607,7 @@
The string key to use for retrieving localized strings
The values to format into the localized string
An associated error record
+ Whether to use the error record for logging the callstack
The entry that is being written
@@ -4941,6 +4961,11 @@
Define the message prefix value for the important level
+
+
+ Global feature flag, to have log messages written with an ErrorRecord object use the ScriptStackTrace of the record, rather than its own.
+
+
The size of the transform error queue. When adding more than this, the oldest entry will be discarded
@@ -5323,6 +5348,11 @@
The message the poor user was shown.
+
+
+ The location where the error happened and how we got there.
+
+
Displays the name of the exception, the make scanning exceptions easier.
@@ -8049,11 +8079,22 @@
When was the agent last active?
+
+
+ What we are currently working on.
+
+
+
+
+ ID of the runspace operated by this Agent
+
+
Create a new agent from a worker
- The parent worker the agent is part of
+ The parent worker the agent is part of.
+ The ID of this specific agent.
@@ -8091,6 +8132,16 @@
Cleanup everything
+
+
+ Whether a failed operation should be repeated.
+
+ The scriptblock determining whether a retry should be done.
+ What went wrong with the last execution.
+ How many attempts have already been made.
+ The maximum number of retries we are willing to attempt.
+ Whether another attempt should be performed
+
Executes a runspace workitem
@@ -8105,6 +8156,16 @@
Wait until timeout (if any)
+
+
+ Launch the Runspace Agent
+
+
+
+
+ End this agent.
+
+
Applies the name to the runspace managed by this agent.
@@ -8115,6 +8176,12 @@
This entire function will fail on PS4 or older
+
+
+ The text representation of this worker
+
+ Some text
+
What kind of timeout a Runspace Workflow Work Item uses
@@ -8536,6 +8603,11 @@
The base code used to launch the process-code in a V2 Agent
+
+
+ The base code used to launch the retry scriptblock in a V2 Agent
+
+
Name of the Worker. Mostly documentary in nature.
@@ -8712,6 +8784,13 @@
The workflow owning the worker
+
+
+ The worker runtime version.
+ 1 operates in full-script.
+ 2 is orchestrated in C# and implements timeouts / activity
+
+
The runspaces belonging to the worker
@@ -8742,6 +8821,16 @@
Bad language modes or not having configured a workflow is a bad thing.A Count less than 1 is implausible.
+
+
+ Launch the agent runspaces for a Gen 1 Runspace Worker.
+
+
+
+
+ Launch the agent tasks for a Gen 2 Runspace Worker
+
+
Signal all workers to stop and gracefully end them.
@@ -8795,6 +8884,14 @@
The error that happened
The target that was being processed as the error happened
+
+
+ Add an error with a target to the errors queue
+
+ The error that happened
+ The target that was being processed as the error happened
+ ID of the runspace that generated the error
+
Something went wrong.
@@ -8840,6 +8937,15 @@
The error that happened
The current item where processing failed
+
+
+ Create a new error object for tracking purposes.
+
+ The worker that failed
+ The error that happened
+ The current item where processing failed
+ The ID of the runspace that encountered the problem
+
Text representation of what went wrong
@@ -8881,6 +8987,42 @@
PowerShell runspace executing the actual code of the worker
+
+
+ An object to process by the runspace workflow
+
+
+
+
+ The entry to process
+
+
+
+
+ How to handle timeout during processing.
+
+
+
+
+ The timeout to apply
+
+
+
+
+ How many times to attempt processing this object in case of error
+
+
+
+
+ Condition under which the item should be attempted again
+
+
+
+
+ Creates a new runspace workflow work item
+
+ The object to process
+
Wrapper class that offers the tools to make Values runspace specific
@@ -10674,6 +10816,28 @@
A generic item type
+
+
+ A dynamic content object that implements a dictionary
+
+
+
+
+ The value of the dynamic content object
+
+
+
+
+ Creates a dynamic content object concurrent dictionary
+
+ The name of the setting
+ The initial value of the object
+
+
+
+ Resets the stack by reestablishing an empty dictionary.
+
+
A dynamic content object that implements a dictionary
@@ -10798,7 +10962,12 @@
- TUrns the value into a concurrent dictionary with case-insensitive string keys
+ Turns the value into a concurrent dictionary with case-insensitive string keys
+
+
+
+
+ Turns the value into a concurrent in-memory cache.
@@ -10844,6 +11013,11 @@
A dictionary was requested
+
+
+ A cache was requested
+
+
A dynamic content object that implements a queue
diff --git a/PSFramework/changelog.md b/PSFramework/changelog.md
index 3e9780cf..be8779f4 100644
--- a/PSFramework/changelog.md
+++ b/PSFramework/changelog.md
@@ -1,5 +1,16 @@
# CHANGELOG
+## 1.14.449 (2026-06-12)
+
+- New: Configuration PSFramework.Message.LogErrorStack - When calling Write-PSFMessage and also specifying an ErrorRecord, should the ErrorRecord location of the error be used, rather than from where Write-PSFMessage was called?
+- New: Reset-PSFRsAgentInactivity - Signals the current Runspace Workflow Worker Agent is active.
+- Upd: Write-PSFMessage - added parameter `-ErrorStack` - logs the stacktrace of the error record provided, rather than its own call.
+- Upd: Add-PSFRunspaceWorker - added new generation of runspace workers, with a c#-based orchestration infrastructure,
+- Upd: Add-PSFRunspaceWorker - added option to retry failed input (including an optional condition logic on when to retry)
+- Upd: Add-PSFRunspaceWorker - added option to include per-item timeout (total time or idle time)
+- Upd: Set-PSFDynamicContentObject - add `-Cache` parameter, generating a DCO implementing a PSFramework cache (same as returned by New-PSFCache)
+- Fix: New-PSFCache - the cache object does not correctly remove objects
+
## 1.14.441 (2026-06-03)
- Fix: New-PSFCache - timer only removes one expired item at a time
diff --git a/PSFramework/en-us/PSFramework.dll-Help.xml b/PSFramework/en-us/PSFramework.dll-Help.xml
index b346edd4..7272ce04 100644
--- a/PSFramework/en-us/PSFramework.dll-Help.xml
+++ b/PSFramework/en-us/PSFramework.dll-Help.xml
@@ -1,5 +1,110 @@
+
+
+ Assert-PSFInternalCommand
+ Assert
+ PSFInternalCommand
+
+ Verifies, that the command calling it in turn was only called from another command within the same module.
+
+
+
+ Verifies, that the command calling it in turn was only called from another command within the same module.
+ Modules can have their internal commands accessed directly from outside of their own module. For example, by loading the psm1 file - or their own ps1 file, if they are shipped separately - it becomes possible, to circumvent the verified interface of the publicly exposed command. In a secure code management scenario, where validated modules are allowlisted, this might allow attackers to execute commands flagged as trusted, that should not be exposed directly.
+ With this command, we can prevent this issue, but potentially make development harder. It is recommended to place this command only as part of the build step - for example by putting it into the code commented out, then remove the comment during build, before publishing the module.
+
+
+
+ Assert-PSFInternalCommand
+
+ PSCmdlet
+
+ The $PSCmdlet variable of the calling command. This ensures that any exceptions are thrown in the context of the calling command, making this Cmdlet functionally invisible.
+
+ PSCmdlet
+
+ PSCmdlet
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ PSCmdlet
+
+ The $PSCmdlet variable of the calling command. This ensures that any exceptions are thrown in the context of the calling command, making this Cmdlet functionally invisible.
+
+ PSCmdlet
+
+ PSCmdlet
+
+
+ None
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+ System.Object
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -------------------------- Example 1 --------------------------
+ PS C:\> Assert-PSFInternalCommand -PSCmdlet $PSCmdlet
+
+ Ensures the current command calling Assert-PSFInternalCommand was only called from another command in the same module.
+
+
+
+
+ ConvertTo-PSFHashtable
@@ -148,6 +253,17 @@
None
+
+ InheritParameters
+
+ Automatically include all values from the parameters of the calling command or script. This will include all parameters that were explicitly bound or have default parameter values, unless they have been excluded or are not part of a spceified -ReferenceCommand. Combine with -IncludeEmpty to also include parameters with neither default value nor being bound.
+
+
+ SwitchParameter
+
+
+ False
+
@@ -283,6 +399,18 @@
None
+
+ InheritParameters
+
+ Automatically include all values from the parameters of the calling command or script. This will include all parameters that were explicitly bound or have default parameter values, unless they have been excluded or are not part of a spceified -ReferenceCommand. Combine with -IncludeEmpty to also include parameters with neither default value nor being bound.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
@@ -732,6 +860,17 @@
None
+
+ NonTerminating
+
+ Make the final error generated a nonterminating error, if the wrapped command fails. By default, exceptions generated will be terminating instead.
+
+
+ SwitchParameter
+
+
+ False
+ Invoke-PSFProtectedCommand
@@ -963,6 +1102,17 @@
None
+
+ NonTerminating
+
+ Make the final error generated a nonterminating error, if the wrapped command fails. By default, exceptions generated will be terminating instead.
+
+
+ SwitchParameter
+
+
+ False
+
@@ -1209,6 +1359,18 @@
None
+
+ NonTerminating
+
+ Make the final error generated a nonterminating error, if the wrapped command fails. By default, exceptions generated will be terminating instead.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
@@ -3961,6 +4123,87 @@ $obj | Select-PSFObject Name, "ID from list WHERE Type = Name"
+
+
+ Update-PSFTeppCompletion
+ Update
+ PSFTeppCompletion
+
+ Imports provided values to parameters configured for an auto-training argument completer.
+
+
+
+ When registering an Argument Completer using "Register-PSFTeppScriptblock", it is possible to configure it for automatic training ("-AutoTraining"). Doing so will not do anything ... by itself. When you apply the argument completer to a parameter, it is now essential to do so via attribute and not via "Register-PSFTeppArgumentCompleter", as the automatic training depends on the attribute.
+ Finally, within your command, simply call this Cmdlet - it will automatically match bound parameters against parameter attributes and add all applicable values to the completion cache, allowing the completion to offer the new values henceforth.
+ For finer control of adding explicit values to a specific completer's offered values, use "Add-PSFTeppCompletion" instead.
+
+
+
+ Update-PSFTeppCompletion
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ ProgressAction
+
+ {{ Fill ProgressAction Description }}
+
+ ActionPreference
+
+ ActionPreference
+
+
+ None
+
+
+
+
+
+ None
+
+
+
+
+
+
+
+
+
+ System.Object
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -------------------------- Example 1 --------------------------
+ PS C:\> Update-PSFTeppCompletion
+
+ Imports provided values to parameters configured for an auto-training argument completer.
+
+
+
+
+ Write-PSFMessage
@@ -4213,6 +4456,28 @@ $obj | Select-PSFObject Name, "ID from list WHERE Type = Name"
None
+
+ ErrorStack
+
+ The log message should have the script-stacktrace of the error record if specified.
+
+
+ SwitchParameter
+
+
+ False
+
+
+ NewErrorRecord
+
+ Create a new error record, using the current message, then write it. This parameter is only respected if EnableException is set to true.
+
+
+ SwitchParameter
+
+
+ False
+ Write-PSFMessage
@@ -4446,6 +4711,28 @@ $obj | Select-PSFObject Name, "ID from list WHERE Type = Name"
None
+
+ ErrorStack
+
+ The log message should have the script-stacktrace of the error record if specified.
+
+
+ SwitchParameter
+
+
+ False
+
+
+ NewErrorRecord
+
+ Create a new error record, using the current message, then write it. This parameter is only respected if EnableException is set to true.
+
+
+ SwitchParameter
+
+
+ False
+
@@ -4694,6 +4981,30 @@ $obj | Select-PSFObject Name, "ID from list WHERE Type = Name"
None
+
+ ErrorStack
+
+ The log message should have the script-stacktrace of the error record if specified.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
+
+ NewErrorRecord
+
+ Create a new error record, using the current message, then write it. This parameter is only respected if EnableException is set to true.
+
+ SwitchParameter
+
+ SwitchParameter
+
+
+ False
+
diff --git a/PSFramework/en-us/stringsRunspaces.psd1 b/PSFramework/en-us/stringsRunspaces.psd1
index 762e6371..085ce222 100644
--- a/PSFramework/en-us/stringsRunspaces.psd1
+++ b/PSFramework/en-us/stringsRunspaces.psd1
@@ -1,6 +1,7 @@
@{
'Add-PSFRunspaceWorker.Error.UntrustedFunctionCode' = 'Failed to load function {0}: The provided function code is not trusted (in Constrained language Mode) and cannot be imported. Ensure the code building the scriptblock is trusted to create a non-constrained scriptblock.' # $pair.Key
'Add-PSFRunspaceWorker.Error.UntrustedTextFunction' = 'Failed to load function {0}: String-based code is not trusted in a secured console. Provide its code as a scriptblock, rather than a string to enable code trust verification.' # $pair.Key
+ 'Add-PSFRunspaceWorker.Error.VersionTooLow' = 'Invalid Runspace Worker Version specified! Version specified: {0} | Minimum Version required for the selected parameters: {1}' # $WorkerVersion, $useVersion
'Invoke-PSFRunspace.Error.ModuleImport' = 'Failed to include module: "{0}"' # $module
'Invoke-PSFRunspace.Error.UntrustedTextFunction' = 'Failed to import function "{0}". Providing function-code as text is not supported in a hardened PowerShell process. Provide the function-code instead as a scriptblock.' # $pair.Key
diff --git a/PSFramework/functions/runspace/Add-PSFRunspaceWorker.ps1 b/PSFramework/functions/runspace/Add-PSFRunspaceWorker.ps1
index 46f8e0e8..d4df1d64 100644
--- a/PSFramework/functions/runspace/Add-PSFRunspaceWorker.ps1
+++ b/PSFramework/functions/runspace/Add-PSFRunspaceWorker.ps1
@@ -13,6 +13,16 @@
In the wider flow of a Runspace Workflow, one Worker's Output Queue is alse another Worker's Input Queue.
Thus we create a chain of workers from original input to finished output, each step individually with as many runspaces as needed.
+
+ Note: Worker Versions
+ There are different runtime versions for the Runspace Workers.
+ They affect features available, performance, and - possibly - bugs.
+ Older (lower) versions are more tested, but some features are only available with later versions.
+ If you encounter an issue with any of the later versions, that works on an older one, please file a bug report and provide as much information as possible.
+
+ Versions and their features:
+ 1: Baseline
+ 2: Added RetryCount, RetryCondition, Timeout, TimeoutType parameters, as well as support for overriding settings per-item with New-PSFRsWorkItem
.PARAMETER Name
Name of the worker.
@@ -104,6 +114,33 @@
.PARAMETER SessionState
A fully prepared session state object to use when creating the worker runspaces.
Be aware that if your session state does not contain basic language tools, the background runspace will likely fail.
+
+ .PARAMETER Timeout
+ How long each individual item may run before timing out.
+ Note: This parameter forces a V2 worker or later (see description).
+
+ .PARAMETER TimeoutType
+ What kind of timeout processing we perform.
+ - Start: Time from the start of the current item.
+ - Idle: Time since last activity
+ Last Activity is measured by the last Write-PSFMessage or Reset-PSFRsAgentInactivity call.
+ Note: This parameter forces a V2 worker or later (see description).
+
+ .PARAMETER RetryCount
+ How many times to try again if processing an object fails.
+ Note: This parameter forces a V2 worker or later (see description).
+
+ .PARAMETER RetryCondition
+ If an object fails and retries are configured, only retries are attempted for cases where this condition is true.
+ This scriptblock has access to two variables:
+ - $_: The Error that happened
+ - $this: The object currently being processed
+ It is executed in the context of the runspace where the issue happend (so modules and commands are available, but the direct scope of the execution code is not.)
+ Note: This parameter forces a V2 worker or later (see description).
+
+ .PARAMETER WorkerVersion
+ What version of worker to create.
+ Later versions offer more features, older versions more stability.
.PARAMETER WorkflowName
Name of the Runspace Workflow this worker belongs to.
@@ -191,6 +228,22 @@
[initialsessionstate]
$SessionState,
+ [PSFTimeSpanParameter]
+ $Timeout,
+
+ [PSFramework.Runspace.RSTimeout]
+ $TimeoutType = 'Start',
+
+ [int]
+ $RetryCount,
+
+ [PsfScriptBlock]
+ $RetryCondition,
+
+ [ValidateSet(1,2)]
+ [int]
+ $WorkerVersion,
+
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[PsfArgumentCompleter('PSFramework-runspace-workflow-name')]
[string[]]
@@ -202,6 +255,26 @@
)
begin {
+ $versionMap = @{
+ 2 = @('Timeout', 'RetryCount', 'RetryCondition')
+ }
+ $useVersion = 1
+
+ foreach ($version in $versionMap.Keys | Sort-Object) {
+ foreach ($parameterName in $versionMap[$version]) {
+ if ($PSBoundParameters.ContainsKey($parameterName)) {
+ $useVersion = $version
+ }
+ }
+ }
+
+ if ($WorkerVersion) {
+ if ($WorkerVersion -lt $useVersion) {
+ Stop-PSFFunction -String 'Add-PSFRunspaceWorker.Error.VersionTooLow' -StringValues $WorkerVersion, $useVersion -EnableException $true -Cmdlet $PSCmdlet -Category InvalidArgument
+ }
+ $useVersion = $WorkerVersion
+ }
+
$functionsResolved = @{ }
if (-not $Functions) { return }
@@ -227,12 +300,19 @@
foreach ($resolvedWorkflow in $resolvedWorkflows) {
$worker = $resolvedWorkflow.AddWorker($Name, $InQueue, $OutQueue, $ScriptBlock, $Count)
+ $worker.WorkerVersion = $useVersion
if ($Begin) { $worker.Begin = $Begin }
if ($End) { $worker.End = $End }
if ($MaxItems) { $worker.MaxItems = $MaxItems }
if ($CloseOutQueue) { $worker.CloseOutQueue = $true }
if ($QueuesToClose) { $worker.QueuesToClose = $QueuesToClose }
+ if ($Timeout) {
+ $worker.Timeout = $Timeout
+ $worker.TimeoutType = $TimeoutType
+ }
+ if ($RetryCount) { $worker.RetryCount = $RetryCount }
+ if ($RetryCondition) { $worker.RetryCondition = $RetryCondition }
if ($SessionState) { $worker.SessionState = $SessionState }
foreach ($module in $Modules) { $worker.Modules.Add($module) }
diff --git a/PSFramework/functions/runspace/Reset-PSFRsAgentInactivity.ps1 b/PSFramework/functions/runspace/Reset-PSFRsAgentInactivity.ps1
new file mode 100644
index 00000000..41d519e6
--- /dev/null
+++ b/PSFramework/functions/runspace/Reset-PSFRsAgentInactivity.ps1
@@ -0,0 +1,24 @@
+function Reset-PSFRsAgentInactivity {
+ <#
+ .SYNOPSIS
+ Signals the current Runspace Workflow Worker Agent(tm) is active.
+
+ .DESCRIPTION
+ Signals the current Runspace Workflow Worker Agent is active.
+ When called from within the code of a Runspace Workflow - specifically, within the code operated by a Generation 2+ Worker - it signals to the Worker-Agent that the current workload is being processed and is not hanging.
+
+ This is used by Generation 2+ Workers when they are configure for timeout type "Idle", where a timeout is performed based on how long the script code has not shown a sign of activity.
+ An alternative way of showing activity is using the Write-PSFMessage command.
+
+ .EXAMPLE
+ PS C:\> Reset-PSFRsAgentInactivity
+
+ Signals the current Runspace Workflow Worker Agent is active.
+ #>
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
+ [CmdletBinding()]
+ param ()
+ process {
+ [PSFramework.Runspace.RunspaceHost]::SignalActive()
+ }
+}
\ No newline at end of file
diff --git a/PSFramework/functions/runspace/Set-PSFDynamicContentObject.ps1 b/PSFramework/functions/runspace/Set-PSFDynamicContentObject.ps1
index 40c585e6..f4db4f71 100644
--- a/PSFramework/functions/runspace/Set-PSFDynamicContentObject.ps1
+++ b/PSFramework/functions/runspace/Set-PSFDynamicContentObject.ps1
@@ -39,6 +39,11 @@
Set the object to be a threadsafe dictionary.
Safe to use in multiple runspaces in parallel.
Will not apply changes if the current value is already such an object.
+
+ .PARAMETER Cache
+ Set the object to be a threadsafe cache.
+ Safe to use in multiple runspaces in parallel.
+ Will not apply changes if the current value is already such an object.
.PARAMETER PassThru
Has the command returning the object just set.
@@ -90,6 +95,10 @@
[switch]
$Dictionary,
+ [Parameter(Mandatory = $true, ParameterSetName = 'Cache')]
+ [switch]
+ $Cache,
+
[switch]
$PassThru,
@@ -105,6 +114,7 @@
elseif ($Stack) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Stack') }
elseif ($List) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'List') }
elseif ($Dictionary) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Dictionary') }
+ elseif ($Cache) { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Cache') }
else { [PSFramework.Utility.DynamicContentObject]::Set($item, $Value, 'Common') }
if ($PassThru) { [PSFramework.Utility.DynamicContentObject]::Get($item) }
@@ -117,6 +127,7 @@
if ($Stack) { $item.ConcurrentStack($Reset) }
if ($List) { $item.ConcurrentList($Reset) }
if ($Dictionary) { $item.ConcurrentDictionary($Reset) }
+ if ($Cache) { $item.ConcurrentCache($Reset) }
if ($PassThru) { $item }
}
diff --git a/PSFramework/internal/configurations/message.ps1 b/PSFramework/internal/configurations/message.ps1
index 9c020983..077fb771 100644
--- a/PSFramework/internal/configurations/message.ps1
+++ b/PSFramework/internal/configurations/message.ps1
@@ -1,4 +1,5 @@
+Set-PSFConfig -Module PSFramework -Name 'Message.LogErrorStack' -Value $false -Initialize -Validation "bool" -Handler { [PSFramework.Message.MessageHost]::LogErrorStack = $_ } -Description "When calling Write-PSFMessage and also specifying an ErrorRecord, should the ErrorRecord location of the error be used, rather than from where Write-PSFMessage was called?"
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Minimum' -Value 1 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumInformation = $_ } -Description "The minimum required message level for messages that will be shown to the user."
Set-PSFConfig -Module PSFramework -Name 'Message.Info.Maximum' -Value 3 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MaximumInformation = $_ } -Description "The maximum message level to still display to the user directly."
Set-PSFConfig -Module PSFramework -Name 'Message.Verbose.Minimum' -Value 4 -Initialize -Validation "integer0to9" -Handler { [PSFramework.Message.MessageHost]::MinimumVerbose = $_ } -Description "The minimum required message level where verbose information is written."
diff --git a/PSFramework/internal/scripts/runspaceWorkerCode.ps1 b/PSFramework/internal/scripts/runspaceWorkerCode.ps1
index 1267ec8c..56e1089b 100644
--- a/PSFramework/internal/scripts/runspaceWorkerCode.ps1
+++ b/PSFramework/internal/scripts/runspaceWorkerCode.ps1
@@ -57,6 +57,9 @@
Start-Sleep -Milliseconds 250
continue
}
+ if ($inputData -is [PSFramework.Runspace.RSWorkItem]) {
+ $inputData = $inputData.Item
+ }
try {
$results = $__PSF_ScriptBlock.InvokeGlobal($inputData)
@@ -87,9 +90,32 @@
[PSFramework.Runspace.RSWorker]::WorkerBeginCode = {
param ($Code)
- & $Code
+ $ErrorActionPreference = 'Stop'
+ try { $Code.InvokeGlobal() }
+ catch { throw $_ }
}
[PSFramework.Runspace.RSWorker]::WorkerProcessCode = {
param ($Code, $Data)
- $Code.InvokeGlobal($Data)
+ $ErrorActionPreference = 'Stop'
+ try {
+ $results = $Code.InvokeGlobal($Data)
+ }
+ catch { throw $_ }
+ foreach ($result in $results) {
+ if ($__PSF_Worker.NoOutput) { break }
+ $__PSF_Workflow.Queues.$($__PSF_Worker.OutQueue).Enqueue($result)
+ $__PSF_Worker.IncrementOutput()
+ }
+}
+[PSFramework.Runspace.RSWorker]::WorkerRetryCode = {
+ param ($Code, $ErrorRecord, $Item)
+ $Code.InvokeEx(
+ $false, # Do not dotsource
+ $ErrorRecord, # $_
+ $null, # $input
+ $Item.Item, # $this
+ $true, # Do import into a context (the local one by default)
+ $true, # Do use the global context for the import, not the local one
+ $null # $args
+ )
}
\ No newline at end of file
diff --git a/PSFramework/tests/functions/runspace/end-to-end.Tests.ps1 b/PSFramework/tests/functions/runspace/end-to-end.Tests.ps1
index 6cd31756..a37f7ca0 100644
--- a/PSFramework/tests/functions/runspace/end-to-end.Tests.ps1
+++ b/PSFramework/tests/functions/runspace/end-to-end.Tests.ps1
@@ -1,4 +1,4 @@
-Describe "Testing the End-To-End Workflows" -Tag "CI", "Pipeline", "Inegration" {
+Describe "Testing the End-To-End Runspace Workflows" -Tag "CI", "Pipeline", "Inegration" {
BeforeEach {
& (Get-Module PSFramework) { $script:runspaceWorkflows = @{ } }
}
@@ -9,14 +9,14 @@
It "Should pass through input to output" {
$workflow = New-PSFRunspaceWorkflow -Name "Test"
- $workflow | Add-PSFRunspaceWorker -Name Node1 -InQueue Q1 -OutQueue Q2 -Count 1 -ScriptBlock {
+ $null = $workflow | Add-PSFRunspaceWorker -Name Node1 -InQueue Q1 -OutQueue Q2 -Count 1 -ScriptBlock {
param ($Value)
$Value
}
- $workflow | Add-PSFRunspaceWorker -Name Node2 -InQueue Q2 -OutQueue Q3 -Count 1 -ScriptBlock {
+ $null = $workflow | Add-PSFRunspaceWorker -Name Node2 -InQueue Q2 -OutQueue Q3 -Count 1 -ScriptBlock {
$_
}
- $workflow | Add-PSFRunspaceWorker -Name Node3 -InQueue Q3 -OutQueue Q4 -Count 1 -ScriptBlock {
+ $null = $workflow | Add-PSFRunspaceWorker -Name Node3 -InQueue Q3 -OutQueue Q4 -Count 1 -ScriptBlock {
param ($Value)
$Value
}
@@ -29,4 +29,264 @@
$results.Count | Should -Be 10
($results | Measure-Object -Sum).Sum | Should -Be 55
}
+
+ It "Should pass through input to output with V2 Workers" {
+ $workflow = New-PSFRunspaceWorkflow -Name "Test V2"
+ $workflow | Add-PSFRunspaceWorker -Name Node1 -InQueue Q1 -OutQueue Q2 -Count 1 -ScriptBlock {
+ param ($Value)
+ $Value
+ } -WorkerVersion 2
+ $workflow | Add-PSFRunspaceWorker -Name Node2 -InQueue Q2 -OutQueue Q3 -Count 1 -ScriptBlock {
+ $_
+ } -WorkerVersion 2
+ $workflow | Add-PSFRunspaceWorker -Name Node3 -InQueue Q3 -OutQueue Q4 -Count 1 -ScriptBlock {
+ param ($Value)
+ $Value
+ } -WorkerVersion 2
+ 1..10 | ForEach-Object { Write-PSFRunspaceQueue -Name Q1 -Value $_ -InputObject $workflow }
+ $workflow | Start-PSFRunspaceWorkflow
+ Start-Sleep -Seconds 1
+
+ $results = Read-PSFRunspaceQueue -InputObject $workflow -Name Q4 -All
+
+ $results.Count | Should -Be 10
+ ($results | Measure-Object -Sum).Sum | Should -Be 55
+ }
+
+ It "Should Respect per-item timeouts" {
+ Clear-PSFMessage
+ $workflow = New-PSFRunspaceWorkflow -Name 'ExampleWorkflow'
+ $worker = $workflow | Add-PSFRunspaceWorker -Name Processing -InQueue Input -OutQueue Done -Count 1 -ScriptBlock {
+ Write-PSFMessage "Start"
+ Start-Sleep -Seconds $_
+ Write-PSFMessage "Done"
+ $_
+ } -CloseOutQueue -Timeout '3s' -TimeoutType 'Start'
+ $workflow | Write-PSFRunspaceQueue -Name Input -BulkValues (1..5) -Close
+ $workflow | Start-PSFRunspaceWorkflow
+
+ $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Closed -PassThru -Timeout '1m' | Stop-PSFRunspaceWorkflow
+ $results = $workflow | Read-PSFRunspaceQueue -Name Done -All
+ $workflow | Remove-PSFRunspaceWorkflow
+ $messages = Get-PSFMessage
+
+ $results | Should -Be 1, 2
+ $worker.Errors | Should -HaveCount 3
+ $messages | Should -HaveCount 7
+ $messages | Where-Object Message -EQ Done | Should -HaveCount 2
+ $messages | Where-Object Message -EQ Start | Should -HaveCount 5
+ $($worker.Errors)[0].Error.ToString() | Should -Be 'Workitem timed out! ExampleWorkflow>Processing>0: 3'
+ $($worker.Errors)[1].Error.ToString() | Should -Be 'Workitem timed out! ExampleWorkflow>Processing>0: 4'
+ $($worker.Errors)[2].Error.ToString() | Should -Be 'Workitem timed out! ExampleWorkflow>Processing>0: 5'
+ }
+
+ It "Should Respect Idle Timeouts - Expired" {
+ Clear-PSFMessage
+ $workflow = New-PSFRunspaceWorkflow -Name 'ExampleWorkflow'
+ $worker = $workflow | Add-PSFRunspaceWorker -Name Processing -InQueue Input -OutQueue Done -Count 1 -ScriptBlock {
+ Write-PSFMessage "Start"
+ Start-Sleep -Seconds 3
+ Write-PSFMessage "Done"
+ $_
+ } -CloseOutQueue -Timeout '2s' -TimeoutType 'Idle'
+ $workflow | Write-PSFRunspaceQueue -Name Input -Value 1 -Close
+ $workflow | Start-PSFRunspaceWorkflow
+
+ $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Closed -PassThru -Timeout '1m' | Stop-PSFRunspaceWorkflow
+ $results = $workflow | Read-PSFRunspaceQueue -Name Done -All
+ $workflow | Remove-PSFRunspaceWorkflow
+ $messages = Get-PSFMessage
+
+ $results | Should -BeNullOrEmpty
+ $worker.Errors | Should -HaveCount 1
+ $messages | Should -HaveCount 1
+ $messages | Where-Object Message -EQ Done | Should -HaveCount 0
+ $messages | Where-Object Message -EQ Start | Should -HaveCount 1
+ $($worker.Errors)[0].Error.ToString() | Should -Be 'Workitem timed out! ExampleWorkflow>Processing>0: 1'
+ }
+
+ It "Should Respect Idle Timeouts - Worked" {
+ Clear-PSFMessage
+ $workflow = New-PSFRunspaceWorkflow -Name 'ExampleWorkflow'
+ $worker = $workflow | Add-PSFRunspaceWorker -Name Processing -InQueue Input -OutQueue Done -Count 1 -ScriptBlock {
+ Write-PSFMessage "Start"
+ Start-Sleep -Seconds 1
+ Write-PSFMessage "Processing"
+ Start-Sleep -Seconds 1
+ Write-PSFMessage "Processing"
+ Start-Sleep -Seconds 1
+ Write-PSFMessage "Done"
+ $_
+ } -CloseOutQueue -Timeout '2s' -TimeoutType 'Idle'
+ $workflow | Write-PSFRunspaceQueue -Name Input -Value 1 -Close
+ $workflow | Start-PSFRunspaceWorkflow
+
+ $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Closed -PassThru -Timeout '1m' | Stop-PSFRunspaceWorkflow
+ $results = $workflow | Read-PSFRunspaceQueue -Name Done -All
+ $workflow | Remove-PSFRunspaceWorkflow
+ $messages = Get-PSFMessage
+
+ $results | Should -Be 1
+ $worker.Errors | Should -HaveCount 0
+ $messages | Should -HaveCount 4
+ $messages | Where-Object Message -EQ Start | Should -HaveCount 1
+ $messages | Where-Object Message -EQ Processing | Should -HaveCount 2
+ $messages | Where-Object Message -EQ Done | Should -HaveCount 1
+ }
+
+ It "Should retry as configured" {
+ Clear-PSFMessage
+ $workflow = New-PSFRunspaceWorkflow -Name 'ExampleWorkflow'
+ $worker = $workflow | Add-PSFRunspaceWorker -Name Processing -InQueue Input -OutQueue Done -Count 1 -ScriptBlock {
+ Write-PSFMessage "Start"
+ 1 / 0
+ Write-PSFMessage "Done"
+ $_
+ } -CloseOutQueue -RetryCount 3
+ $workflow | Write-PSFRunspaceQueue -Name Input -Value 1 -Close
+ $workflow | Start-PSFRunspaceWorkflow
+
+ $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Closed -PassThru -Timeout '1m' | Stop-PSFRunspaceWorkflow
+ $results = $workflow | Read-PSFRunspaceQueue -Name Done -All
+ $workflow | Remove-PSFRunspaceWorkflow
+ $messages = Get-PSFMessage
+
+ $results | Should -BeNullOrEmpty
+ $worker.Errors | Should -HaveCount 1
+ $messages | Should -HaveCount 4
+ $messages | Where-Object Message -EQ Start | Should -HaveCount 4
+ $messages | Where-Object Message -EQ Done | Should -HaveCount 0
+ $($worker.Errors)[0].Error.ToString() | Should -Be 'Attempted to divide by zero.'
+ }
+
+ Context "Should retry as configured and applicable" {
+ BeforeAll {
+ Clear-PSFMessage
+ $workflow = New-PSFRunspaceWorkflow -Name 'ExampleWorkflow'
+ $worker = $workflow | Add-PSFRunspaceWorker -Name Processing -InQueue Input -OutQueue Done -Count 1 -Scriptblock {
+ Write-PSFMessage "Start: $_"
+ 1 / 0
+ Write-PSFMessage "Done: $_"
+ $_
+ } -CloseOutQueue -RetryCount 3 -RetryCondition { $this -eq 1 }
+ $workflow | Write-PSFRunspaceQueue -Name Input -BulkValues 1, 2 -Close
+ $workflow | Start-PSFRunspaceWorkflow
+
+ $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Closed -PassThru -Timeout '1m' | Stop-PSFRunspaceWorkflow
+ $results = $workflow | Read-PSFRunspaceQueue -Name Done -All
+ $workflow | Remove-PSFRunspaceWorkflow
+ $messages = Get-PSFMessage
+ }
+
+ It "Should have no results" {
+ $results | Should -BeNullOrEmpty
+ }
+
+ It "Should have failed twice and correctly" {
+ $worker.Errors | Should -HaveCount 2
+ $($worker.Errors)[0].Error.ToString() | Should -Be 'Attempted to divide by zero.'
+ $($worker.Errors)[1].Error.ToString() | Should -Be 'Attempted to divide by zero.'
+ }
+
+ It "Should have generated 5 messages, none completed" {
+ $messages | Should -HaveCount 5
+ $messages | Where-Object Message -EQ 'Start: 1' | Should -HaveCount 4
+ $messages | Where-Object Message -EQ 'Start: 2' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ Done | Should -HaveCount 0
+ }
+ }
+
+ Context "Should execute Begin, Process, and End correctly V1" {
+ BeforeAll {
+ Clear-PSFMessage
+ $workflow = New-PSFRunspaceWorkflow -Name 'ExampleWorkflow'
+ $worker = $workflow | Add-PSFRunspaceWorker -Name Processing -InQueue Input -OutQueue Done -Count 1 -Begin {
+ Write-PSFMessage "Beginning"
+ } -Scriptblock {
+ Write-PSFMessage "Processing: $_"
+ $_
+ } -End {
+ Write-PSFMessage "Ending"
+ } -CloseOutQueue
+ $workflow | Write-PSFRunspaceQueue -Name Input -BulkValues (1..5) -Close
+ $workflow | Start-PSFRunspaceWorkflow
+
+ $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Closed -PassThru -Timeout '1m' | Stop-PSFRunspaceWorkflow
+ $results = $workflow | Read-PSFRunspaceQueue -Name Done -All
+ $workflow | Remove-PSFRunspaceWorkflow
+ $messages = Get-PSFMessage
+ }
+
+ It "Should have 5 result numbers - 1, 2, 3, 4, 5" {
+ $results | Should -Be 1, 2, 3, 4, 5
+ $results | Should -HaveCount 5
+ }
+
+ It "Should have executed without errors" {
+ $worker.Errors | Should -HaveCount 0
+ }
+
+ It "Should have the expected 7 Messages" {
+ $messages | Should -HaveCount 7
+ $messages | Where-Object Message -EQ 'Beginning' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 1' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 2' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 3' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 4' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 5' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Ending' | Should -HaveCount 1
+ }
+
+ It "Should have executed in the same Agent Runspace" {
+ $messages | Group-Object Runspace | Should -HaveCount 1
+ $messages | Where-Object Runspace -EQ ([runspace]::DefaultRunspace.InstanceId) | Should -HaveCount 0
+ }
+ }
+
+ Context "Should execute Begin, Process, and End correctly V2" {
+ BeforeAll {
+ Clear-PSFMessage
+ $workflow = New-PSFRunspaceWorkflow -Name 'ExampleWorkflow'
+ $worker = $workflow | Add-PSFRunspaceWorker -Name Processing -InQueue Input -OutQueue Done -Count 1 -Begin {
+ Write-PSFMessage "Beginning"
+ } -Scriptblock {
+ Write-PSFMessage "Processing: $_"
+ $_
+ } -End {
+ Write-PSFMessage "Ending"
+ } -CloseOutQueue -WorkerVersion 2
+ $workflow | Write-PSFRunspaceQueue -Name Input -BulkValues (1..5) -Close
+ $workflow | Start-PSFRunspaceWorkflow
+
+ $workflow | Wait-PSFRunspaceWorkflow -Queue Done -Closed -PassThru -Timeout '1m' | Stop-PSFRunspaceWorkflow
+ $results = $workflow | Read-PSFRunspaceQueue -Name Done -All
+ $workflow | Remove-PSFRunspaceWorkflow
+ $messages = Get-PSFMessage
+ }
+
+ It "Should have 5 result numbers - 1, 2, 3, 4, 5" {
+ $results | Should -Be 1, 2, 3, 4, 5
+ $results | Should -HaveCount 5
+ }
+
+ It "Should have executed without errors" {
+ $worker.Errors | Should -HaveCount 0
+ }
+
+ It "Should have the expected 7 Messages" {
+ $messages | Should -HaveCount 7
+ $messages | Where-Object Message -EQ 'Beginning' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 1' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 2' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 3' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 4' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Processing: 5' | Should -HaveCount 1
+ $messages | Where-Object Message -EQ 'Ending' | Should -HaveCount 1
+ }
+
+ It "Should have executed in the same Agent Runspace" {
+ $messages | Group-Object Runspace | Should -HaveCount 1
+ $messages | Where-Object Runspace -EQ ([runspace]::DefaultRunspace.InstanceId) | Should -HaveCount 0
+ }
+ }
}
\ No newline at end of file
diff --git a/help/en-us/Invoke-PSFProtectedCommand.md b/help/en-us/Invoke-PSFProtectedCommand.md
index 425f8575..99d9ed4f 100644
--- a/help/en-us/Invoke-PSFProtectedCommand.md
+++ b/help/en-us/Invoke-PSFProtectedCommand.md
@@ -385,6 +385,21 @@ Accept pipeline input: False
Accept wildcard characters: False
```
+### -ProgressAction
+{{ Fill ProgressAction Description }}
+
+```yaml
+Type: ActionPreference
+Parameter Sets: (All)
+Aliases: proga
+
+Required: False
+Position: Named
+Default value: None
+Accept pipeline input: False
+Accept wildcard characters: False
+```
+
### -RetryWaitEscalation
When retrying failed attempts, the previous wait time is multiplied by this value.
This allows waiting longer and longer periods, each time it failed.
diff --git a/help/en-us/Write-PSFMessage.md b/help/en-us/Write-PSFMessage.md
index 39246faa..e73a4ece 100644
--- a/help/en-us/Write-PSFMessage.md
+++ b/help/en-us/Write-PSFMessage.md
@@ -18,7 +18,7 @@ Write-PSFMessage [-Level ] -Message [-StringValues ] [-FunctionName ] [-ModuleName ] [-File ] [-Line ]
[-ErrorRecord ] [-Exception ] [-Once ] [-OverrideExceptionMessage]
[-Target
[Parameter()]
public PSCmdlet PSCmdlet;
+
+ ///
+ /// The log message should have the script-stacktrace of the error record if specified
+ ///
+ [Parameter()]
+ public SwitchParameter ErrorStack;
#endregion Parameters
#region Private fields
@@ -568,7 +574,7 @@ whether this function was called from Stop-PSFFunction.
#endregion Message handling
#region Logging
- LogEntry entry = LogHost.WriteLogEntry(_MessageSystem, channels, _timestamp, FunctionName, ModuleName, _Tags, Data, Level, System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId, Environment.MachineName, File, Line, _callStack, String.Format("{0}\\{1}",Environment.UserDomainName, Environment.UserName), errorRecord, String, StringValues, Target);
+ LogEntry entry = LogHost.WriteLogEntry(_MessageSystem, channels, _timestamp, FunctionName, ModuleName, _Tags, Data, Level, System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId, Environment.MachineName, File, Line, _callStack, String.Format("{0}\\{1}",Environment.UserDomainName, Environment.UserName), errorRecord, String, StringValues, Target, ErrorStack.ToBool());
#endregion Logging
foreach (MessageEventSubscription subscription in MessageHost.Events.Values)
diff --git a/library/PSFramework/Message/CallStack.cs b/library/PSFramework/Message/CallStack.cs
index 70e070d1..bf99b731 100644
--- a/library/PSFramework/Message/CallStack.cs
+++ b/library/PSFramework/Message/CallStack.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Management.Automation;
+using System.Text.RegularExpressions;
namespace PSFramework.Message
{
@@ -93,5 +94,21 @@ public CallStack(IEnumerable CallStack)
foreach (CallStackFrame frame in CallStack)
Entries.Add(new CallStackEntry(frame.FunctionName, frame.ScriptName, frame.ScriptLineNumber, frame.InvocationInfo));
}
+
+ ///
+ /// Initialize a callstack from a ScriptCallStack as provided by an ErrorRecord
+ ///
+ /// The ScriptCallStack provided by an ErrorRecord
+ public CallStack(string ScriptStackTrace)
+ {
+ foreach (string line in ScriptStackTrace.Split('\n'))
+ {
+ string command = Regex.Replace(line, "^at (.+?),.+$", "$1", RegexOptions.IgnoreCase);
+ string file = Regex.Replace(line, "^at .+?,(.+): line .+$", "$1", RegexOptions.IgnoreCase);
+ string lineNr = Regex.Replace(line, "^.+?(\\d+)$", "$1");
+
+ Entries.Add(new CallStackEntry(command, file, Int32.Parse(lineNr), null));
+ }
+ }
}
}
diff --git a/library/PSFramework/Message/LogHost.cs b/library/PSFramework/Message/LogHost.cs
index b5975449..bd92813b 100644
--- a/library/PSFramework/Message/LogHost.cs
+++ b/library/PSFramework/Message/LogHost.cs
@@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Linq;
using System.Management.Automation;
namespace PSFramework.Message
@@ -190,9 +191,12 @@ public static void ClearLog()
public static PsfExceptionRecord WriteErrorEntry(ErrorRecord[] Record, string FunctionName, string ModuleName, List Tags, DateTime Timestamp, string Message, Guid Runspace, string ComputerName)
{
PsfExceptionRecord tempRecord = new PsfExceptionRecord(Runspace, ComputerName, Timestamp, FunctionName, ModuleName, Tags, Message);
- foreach (ErrorRecord rec in Record)
+ if (Record != null && Record.Length > 0)
{
- tempRecord.Exceptions.Add(new PsfException(rec, FunctionName, Timestamp, Message, Runspace, ComputerName));
+ foreach (ErrorRecord rec in Record)
+ tempRecord.Exceptions.Add(new PsfException(rec, FunctionName, Timestamp, Message, Runspace, ComputerName));
+
+ tempRecord.ScriptStackTrace = Record[0].ScriptStackTrace;
}
if (ErrorLogFileEnabled) { OutQueueError.Enqueue(tempRecord); }
@@ -225,10 +229,11 @@ public static PsfExceptionRecord WriteErrorEntry(ErrorRecord[] Record, string Fu
/// The callstack at the moment the message was written.
/// The name of the user under which the code being executed
/// An associated error record
+ /// Whether to use the error record for logging the callstack
/// The entry that is being written
- public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, Hashtable Data, MessageLevel Level, Guid Runspace, string ComputerName, string File, int Line, IEnumerable CallStack, string Username, PsfExceptionRecord ErrorRecord, object TargetObject = null)
+ public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, Hashtable Data, MessageLevel Level, Guid Runspace, string ComputerName, string File, int Line, IEnumerable CallStack, string Username, PsfExceptionRecord ErrorRecord, object TargetObject = null, bool ErrorStack = false)
{
- return WriteLogEntry(Message, Type, Timestamp, FunctionName, ModuleName, Tags, Data, Level, Runspace, ComputerName, File, Line, CallStack, Username, ErrorRecord, "", null, TargetObject);
+ return WriteLogEntry(Message, Type, Timestamp, FunctionName, ModuleName, Tags, Data, Level, Runspace, ComputerName, File, Line, CallStack, Username, ErrorRecord, "", null, TargetObject, ErrorStack);
}
///
@@ -252,10 +257,16 @@ public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime
/// The string key to use for retrieving localized strings
/// The values to format into the localized string
/// An associated error record
+ /// Whether to use the error record for logging the callstack
/// The entry that is being written
- public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, Hashtable Data, MessageLevel Level, Guid Runspace, string ComputerName, string File, int Line, IEnumerable CallStack, string Username, PsfExceptionRecord ErrorRecord, string String, object[] StringValue, object TargetObject = null)
+ public static LogEntry WriteLogEntry(string Message, LogEntryType Type, DateTime Timestamp, string FunctionName, string ModuleName, List Tags, Hashtable Data, MessageLevel Level, Guid Runspace, string ComputerName, string File, int Line, IEnumerable CallStack, string Username, PsfExceptionRecord ErrorRecord, string String, object[] StringValue, object TargetObject = null, bool ErrorStack = false)
{
- LogEntry temp = new LogEntry(Message, Type, Timestamp, FunctionName, ModuleName, Tags, Data, Level, Runspace, ComputerName, TargetObject, File, Line, new PSFramework.Message.CallStack(CallStack), Username, ErrorRecord, String, StringValue);
+ CallStack stack;
+ if (ErrorRecord != null && (ErrorStack || MessageHost.LogErrorStack))
+ stack = new CallStack(ErrorRecord.ScriptStackTrace);
+ else
+ stack = new CallStack(CallStack);
+ LogEntry temp = new LogEntry(Message, Type, Timestamp, FunctionName, ModuleName, Tags, Data, Level, Runspace, ComputerName, TargetObject, File, Line, stack, Username, ErrorRecord, String, StringValue);
if (MessageLogFileEnabled) { OutQueueLog.Enqueue(temp); }
if (MessageLogEnabled) { LogEntries.Enqueue(temp); }
diff --git a/library/PSFramework/Message/MessageHost.cs b/library/PSFramework/Message/MessageHost.cs
index 7889fa3d..d93dfd3a 100644
--- a/library/PSFramework/Message/MessageHost.cs
+++ b/library/PSFramework/Message/MessageHost.cs
@@ -145,8 +145,11 @@ public static class MessageHost
/// Define the message prefix value for the important level
///
public static string PrefixValueSignificant = "##[section]";
-
+ ///
+ /// Global feature flag, to have log messages written with an ErrorRecord object use the ScriptStackTrace of the record, rather than its own.
+ ///
+ public static bool LogErrorStack = false;
#endregion Defines
#region Transformations
diff --git a/library/PSFramework/Message/PsfExceptionRecord.cs b/library/PSFramework/Message/PsfExceptionRecord.cs
index 1af0f807..6772eb97 100644
--- a/library/PSFramework/Message/PsfExceptionRecord.cs
+++ b/library/PSFramework/Message/PsfExceptionRecord.cs
@@ -44,6 +44,11 @@ public class PsfExceptionRecord
///
public string Message;
+ ///
+ /// The location where the error happened and how we got there.
+ ///
+ public string ScriptStackTrace;
+
///
/// Displays the name of the exception, the make scanning exceptions easier.
///
diff --git a/library/PSFramework/PSFramework.csproj b/library/PSFramework/PSFramework.csproj
index 7a3a345a..594f26a5 100644
--- a/library/PSFramework/PSFramework.csproj
+++ b/library/PSFramework/PSFramework.csproj
@@ -250,6 +250,7 @@
+
diff --git a/library/PSFramework/Runspace/RSAgent.cs b/library/PSFramework/Runspace/RSAgent.cs
index f2b88fbd..006075bd 100644
--- a/library/PSFramework/Runspace/RSAgent.cs
+++ b/library/PSFramework/Runspace/RSAgent.cs
@@ -1,4 +1,5 @@
-using PSFramework.Utility;
+using PSFramework.PSFCore;
+using PSFramework.Utility;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
@@ -8,6 +9,7 @@
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -27,22 +29,36 @@ public class RSAgent : IDisposable
/// The Worker this Agent belongs to
///
public RSWorker Parent { get; private set; }
+
///
/// When was the agent last active?
///
public DateTime LastActivity { get; private set; }
+ ///
+ /// What we are currently working on.
+ ///
public RSWorkItem CurrentItem;
+ ///
+ /// ID of the runspace operated by this Agent
+ ///
+ public Nullable RunspaceID { get; private set; }
+
private PsfScriptBlock _Begin;
private PsfScriptBlock _Process;
private PsfScriptBlock _End;
- private PowerShell _PSRuntime;
+ internal PowerShell _PSRuntime;
+ internal System.Management.Automation.Runspaces.Runspace _Runspace;
+ private bool _Run;
+
+ internal Task MainTask;
///
/// Create a new agent from a worker
///
- /// The parent worker the agent is part of
+ /// The parent worker the agent is part of.
+ /// The ID of this specific agent.
public RSAgent(RSWorker Parent, int ID)
{
this.Parent = Parent;
@@ -66,8 +82,6 @@ internal void Initialize()
_Process = Parent.ScriptBlock;
_End = Parent.End;
- RunspaceHost.RSAgents[System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId] = this;
-
InitialSessionState localState = Parent.GetSessionState();
foreach (string key in Parent.PerRSValues.Keys)
{
@@ -75,7 +89,10 @@ internal void Initialize()
localState.Variables.Add(new SessionStateVariableEntry(key, data, null));
}
localState.Variables.Add(new SessionStateVariableEntry("__PSF_Agent", this, "The current instance of the worker.", ScopedItemOptions.Constant));
- _PSRuntime = PowerShell.Create(localState);
+ _Runspace = RunspaceFactory.CreateRunspace(localState);
+ _Runspace.Open();
+ RunspaceID = _Runspace.InstanceId;
+ RunspaceHost.RSAgents[RunspaceID.Value] = this;
SetRunspaceName();
}
@@ -85,20 +102,24 @@ internal void Initialize()
internal bool Begin()
{
try {
- _PSRuntime.AddScript(RSWorker.WorkerBeginCode.ToString(), true);
- _PSRuntime.AddArgument(_Begin);
- _PSRuntime.Invoke();
+ using (PowerShell execution = PowerShell.Create())
+ {
+ execution.Runspace = _Runspace;
+ execution.AddScript(RSWorker.WorkerBeginCode.ToString(), true);
+ execution.AddArgument(_Begin);
+ execution.Invoke();
+ }
}
catch (RuntimeException e)
{
Parent.ErrorCount++;
- Parent.LastError = e.ErrorRecord;
+ Parent.AddError(e.ErrorRecord, null, RunspaceID.Value);
return false;
}
catch (Exception e)
{
Parent.ErrorCount++;
- Parent.LastError = new ErrorRecord(e, "RunspaceError", ErrorCategory.NotSpecified, null);
+ Parent.AddError(new ErrorRecord(e, "RunspaceError", ErrorCategory.NotSpecified, null), null, RunspaceID.Value);
return false;
}
return true;
@@ -112,7 +133,45 @@ internal void Process(RSWorkItem Item)
CurrentItem = Item;
SignalActive();
+ int retryCount = Parent.RetryCount;
+ if (Item.RetryCount != null)
+ retryCount = Item.RetryCount.Value;
+
+ PsfScriptBlock retryCondition = Parent.RetryCondition;
+ if (Item.RetryCondition != null)
+ retryCondition = Item.RetryCondition;
+
+ int attempts = 0;
+ Exception lastError;
+ do
+ {
+ try
+ {
+ Invoke(RSWorker.WorkerProcessCode, Item).GetAwaiter().GetResult();
+ Parent.IncrementInputCompleted();
+ return;
+ }
+ catch (Exception e)
+ {
+ attempts++;
+ lastError = e;
+ if (!ShouldRetry(retryCondition, e, attempts, retryCount))
+ break;
+ }
+ }
+ while (attempts <= retryCount);
+
+ ErrorRecord record;
+ if (lastError is RuntimeException)
+ record = ((RuntimeException)lastError).ErrorRecord;
+ else
+ record = new ErrorRecord(lastError, "AgentError", ErrorCategory.NotSpecified, Item.Item);
+
+ Parent.IncrementInputCompleted();
+ Parent.ErrorCount++;
+ if (RunspaceID != null)
+ Parent.AddError(record, Item.Item, RunspaceID.Value);
}
///
/// Execute the end phase
@@ -121,21 +180,23 @@ internal void End()
{
try
{
- _PSRuntime.AddScript(RSWorker.WorkerBeginCode.ToString(), true);
- _PSRuntime.AddArgument(_End);
- _PSRuntime.Invoke();
+ using (PowerShell execution = PowerShell.Create())
+ {
+ execution.Runspace = _Runspace;
+ execution.AddScript(RSWorker.WorkerBeginCode.ToString(), true);
+ execution.AddArgument(_End);
+ execution.Invoke();
+ }
}
catch (RuntimeException e)
{
Parent.ErrorCount++;
- Parent.LastError = e.ErrorRecord;
- return;
+ Parent.AddError(e.ErrorRecord, null, RunspaceID.Value);
}
catch (Exception e)
{
Parent.ErrorCount++;
- Parent.LastError = new ErrorRecord(e, "RunspaceError", ErrorCategory.NotSpecified, null);
- return;
+ Parent.AddError(new ErrorRecord(e, "RunspaceError", ErrorCategory.NotSpecified, null), null, RunspaceID.Value);
}
}
@@ -162,6 +223,8 @@ internal void Execute()
break;
if (Parent.MaxItems > 0 && Parent.MaxItems <= Parent.CountInputCompleted)
break;
+ if (!_Run)
+ break;
if (Parent.Throttle != null)
Parent.Throttle.GetSlot();
@@ -196,8 +259,63 @@ internal void Execute()
public void Dispose()
{
RSAgent temp;
- RunspaceHost.RSAgents.TryRemove(System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId, out temp);
- _PSRuntime.Dispose();
+ if (RunspaceID != null)
+ {
+ RunspaceHost.RSAgents.TryRemove(RunspaceID.Value, out temp);
+ RunspaceID = null;
+ }
+
+ if (_Runspace != null)
+ {
+ _Runspace.Dispose();
+ _Runspace = null;
+ }
+ if (_PSRuntime != null)
+ {
+ _PSRuntime.Dispose();
+ _PSRuntime = null;
+ }
+ }
+
+ ///
+ /// Whether a failed operation should be repeated.
+ ///
+ /// The scriptblock determining whether a retry should be done.
+ /// What went wrong with the last execution.
+ /// How many attempts have already been made.
+ /// The maximum number of retries we are willing to attempt.
+ /// Whether another attempt should be performed
+ internal bool ShouldRetry(PsfScriptBlock Condition, Exception Error, int Attempts, int MaxRetries)
+ {
+ // Exhausted max retry attempts
+ if (Attempts > MaxRetries)
+ return false;
+
+ // No Condition = Always Retry
+ if (Condition == null)
+ return true;
+
+ ErrorRecord errorObject;
+ if (Error is RuntimeException)
+ errorObject = (Error as RuntimeException).ErrorRecord;
+ else
+ errorObject = new ErrorRecord(Error, "InvocationError", ErrorCategory.NotSpecified,CurrentItem.Item);
+
+ try
+ {
+ using (PowerShell runtime = PowerShell.Create())
+ {
+ runtime.Runspace = _Runspace;
+ Collection result = runtime.AddScript(RSWorker.WorkerRetryCode.ToString()).AddArgument(Condition).AddArgument(errorObject).AddArgument(CurrentItem).Invoke();
+ if (result.Count < 1)
+ return false;
+ return LanguagePrimitives.IsTrue(result[0]);
+ }
+ }
+ catch
+ {
+ return false;
+ }
}
///
@@ -213,6 +331,12 @@ public async Task> Invoke(PsfScriptBlock Code, RSWorkItem I
if (CurrentItem.TimeoutType != RSTimeout.Undefined)
timeoutMode = CurrentItem.TimeoutType;
+ // Cleanup a Previous command if cleanup failed
+ if (_PSRuntime != null)
+ _PSRuntime.Dispose();
+
+ _PSRuntime = PowerShell.Create();
+ _PSRuntime.Runspace = _Runspace;
_PSRuntime.AddScript(Code.ToString());
_PSRuntime.AddArgument(_Process).AddArgument(Item.Item);
@@ -221,18 +345,18 @@ public async Task> Invoke(PsfScriptBlock Code, RSWorkItem I
return await execution;
Task waiter = Task.Run(() => Wait());
- Task first = Await(execution, waiter);
+ Task first = (Task)Await(execution, waiter).GetAwaiter().GetResult();
// Case: Did not timeout
if (first == execution)
{
- waiter.Dispose();
+ // waiter.Dispose();
return await execution;
}
// Case: Timeout
waiter.Dispose();
- execution.Dispose();
+ // execution.Dispose();
_PSRuntime.Stop();
throw new TimeoutException($"Workitem timed out! {Parent.Workflow.Name}>{Parent.Name}>{ID}: {CurrentItem.Item}");
}
@@ -274,6 +398,27 @@ public void Wait()
}
}
+ ///
+ /// Launch the Runspace Agent
+ ///
+ public void Start()
+ {
+ _Run = true;
+ Initialize();
+ MainTask = Task.Run(() => Execute());
+ }
+
+ ///
+ /// End this agent.
+ ///
+ public void Stop()
+ {
+ _Run = false;
+ if (!Parent.KillToStop && MainTask != null)
+ MainTask.Wait();
+ Dispose();
+ }
+
#region Utilities
///
/// Applies the name to the runspace managed by this agent.
@@ -291,5 +436,14 @@ private void SetRunspaceNameInternal()
_PSRuntime.Runspace.Name = $"PSF-{Parent.Workflow.Name}-{Parent.Name}-{ID}";
}
#endregion Utilities
+
+ ///
+ /// The text representation of this worker
+ ///
+ /// Some text
+ public override string ToString()
+ {
+ return $"{Parent.Name}-{ID}";
+ }
}
}
diff --git a/library/PSFramework/Runspace/RSWorkItem.cs b/library/PSFramework/Runspace/RSWorkItem.cs
index 1dff8824..12f9c8db 100644
--- a/library/PSFramework/Runspace/RSWorkItem.cs
+++ b/library/PSFramework/Runspace/RSWorkItem.cs
@@ -1,4 +1,5 @@
using PSFramework.Parameter;
+using PSFramework.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,16 +9,40 @@
namespace PSFramework.Runspace
{
+ ///
+ /// An object to process by the runspace workflow
+ ///
public class RSWorkItem
{
+ ///
+ /// The entry to process
+ ///
public object Item;
+ ///
+ /// How to handle timeout during processing.
+ ///
public RSTimeout TimeoutType = RSTimeout.Undefined;
+ ///
+ /// The timeout to apply
+ ///
public TimeSpanParameter Timeout;
- public int RetryCount;
+ ///
+ /// How many times to attempt processing this object in case of error
+ ///
+ public Nullable RetryCount;
+ ///
+ /// Condition under which the item should be attempted again
+ ///
+ public PsfScriptBlock RetryCondition;
+
+ ///
+ /// Creates a new runspace workflow work item
+ ///
+ /// The object to process
public RSWorkItem(object Item)
{
this.Item = Item;
diff --git a/library/PSFramework/Runspace/RSWorker.cs b/library/PSFramework/Runspace/RSWorker.cs
index 3964cc80..8873128c 100644
--- a/library/PSFramework/Runspace/RSWorker.cs
+++ b/library/PSFramework/Runspace/RSWorker.cs
@@ -8,6 +8,7 @@
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -61,6 +62,20 @@ public static PsfScriptBlock WorkerProcessCode
}
private static PsfScriptBlock _WorkerProcessCode;
+ ///
+ /// The base code used to launch the retry scriptblock in a V2 Agent
+ ///
+ public static PsfScriptBlock WorkerRetryCode
+ {
+ get { return _WorkerRetryCode; }
+ set
+ {
+ if (_WorkerRetryCode == null)
+ _WorkerRetryCode = value;
+ }
+ }
+ private static PsfScriptBlock _WorkerRetryCode;
+
///
/// Name of the Worker. Mostly documentary in nature.
///
@@ -258,6 +273,13 @@ public ErrorRecord LastError
///
public RSWorkflow Workflow => workflow;
+ ///
+ /// The worker runtime version.
+ /// 1 operates in full-script.
+ /// 2 is orchestrated in C# and implements timeouts / activity
+ ///
+ public int WorkerVersion = 1;
+
///
/// The runspaces belonging to the worker
///
@@ -270,6 +292,9 @@ public List Runspaces
foreach (RSPowerShellWrapper wrapper in runtimes)
if (wrapper.Pipe != null && wrapper.Pipe.Runspace != null)
result.Add(new RSWorkflowRunespaceReport(workflow, this, wrapper.Pipe.Runspace));
+ foreach (RSAgent agent in agents)
+ if (agent._Runspace != null)
+ result.Add(new RSWorkflowRunespaceReport(workflow, this, agent._Runspace));
return result;
}
@@ -277,6 +302,7 @@ public List Runspaces
private RSWorkflow workflow;
private List runtimes = new List();
+ private List agents = new List();
///
/// Create a new runspace worker
@@ -346,8 +372,10 @@ public void Start()
throw new InvalidOperationException("Refusing to launch worker: The registered worker code is not trusted!");
if (WorkerBeginCode.LanguageMode != PSLanguageMode.FullLanguage)
throw new InvalidOperationException("Refusing to launch worker: The registered Begin worker code is not trusted!");
- if (WorkerBeginCode.LanguageMode != PSLanguageMode.FullLanguage)
+ if (WorkerProcessCode.LanguageMode != PSLanguageMode.FullLanguage)
throw new InvalidOperationException("Refusing to launch worker: The registered Process worker code is not trusted!");
+ if (WorkerRetryCode.LanguageMode != PSLanguageMode.FullLanguage)
+ throw new InvalidOperationException("Refusing to launch worker: The registered Retry worker code is not trusted!");
if (null == workflow)
throw new InvalidOperationException("Runspace Workflow cannot be null!");
@@ -360,51 +388,57 @@ public void Start()
AssertFunctionSafety();
- #region Prepare the Initial Session State
- _Begin = Begin;
- _End = End;
+ State = RSState.Starting;
- InitialSessionState localState = SessionState;
- if (null == localState)
- localState = workflow.SessionState;
- if (null == localState)
- localState = InitialSessionState.CreateDefault();
+ #region Launch Runspaces
+ switch (WorkerVersion)
+ {
+ case 1:
+ LaunchV1();
+ break;
+ case 2:
+ LaunchV2();
+ break;
+ default:
+ LaunchV1();
+ break;
+ }
+ #endregion Launch Runspaces
- if (workflow.Modules.Count > 0)
- localState.ImportPSModule(workflow.Modules.ToArray());
- if (Modules.Count > 0)
- localState.ImportPSModule(Modules.ToArray());
- localState.ImportPSModulesFromPath(PSFCore.PSFCoreHost.ModuleRoot);
+ State = RSState.Running;
+ }
- if (workflow.Functions.Count > 0)
- foreach (string name in workflow.Functions.Keys)
- localState.Commands.Add(new SessionStateFunctionEntry(name, workflow.Functions[name].ToString()));
- if (Functions.Count > 0)
- foreach (string name in Functions.Keys)
- localState.Commands.Add(new SessionStateFunctionEntry(name, Functions[name].ToString()));
+ ///
+ /// Launch the agent runspaces for a Gen 1 Runspace Worker.
+ ///
+ internal void LaunchV1()
+ {
+ #region Prepare the Initial Session State
+ _Begin = Begin;
+ _End = End;
- if (workflow.Variables.Count > 0)
- foreach (string name in workflow.Variables.Keys)
- localState.Variables.Add(new SessionStateVariableEntry(name, workflow.Variables[name], null));
- if (Variables.Count > 0)
- foreach (string name in Variables.Keys)
- localState.Variables.Add(new SessionStateVariableEntry(name, Variables[name], null));
- localState.Variables.Add(new SessionStateVariableEntry("__PSF_Workflow", workflow, "PSF Runspace Workflow, used to manage the data transfer between workers and the state handling of the workload.", ScopedItemOptions.Constant));
- localState.Variables.Add(new SessionStateVariableEntry("__PSF_Worker", this, "PSF Worker. Represents itself in the active runspaces.", ScopedItemOptions.Constant));
+ InitialSessionState localState = GetSessionState();
#endregion Prepare the Initial Session State
- State = RSState.Starting;
-
- #region Launch Runspaces
for (int i = 0; i < Count; i++)
{
PowerShell powershell = PowerShell.Create(localState);
powershell.AddScript(WorkerCode.ToString()).AddArgument(i);
runtimes.Add(new RSPowerShellWrapper(powershell, powershell.BeginInvoke()));
}
- #endregion Launch Runspaces
+ }
- State = RSState.Running;
+ ///
+ /// Launch the agent tasks for a Gen 2 Runspace Worker
+ ///
+ internal void LaunchV2()
+ {
+ for (int i = 0; i < Count; i++)
+ {
+ RSAgent newAgent = new RSAgent(this, i);
+ newAgent.Start();
+ agents.Add(newAgent);
+ }
}
///
@@ -421,6 +455,8 @@ public void Stop()
runtime.Pipe.Runspace.Dispose();
runtime.Pipe.Dispose();
}
+ foreach (RSAgent agent in agents)
+ agent.Stop();
State = RSState.Stopped;
runtimes = new List();
@@ -538,7 +574,26 @@ private void AssertFunctionSafety()
/// The target that was being processed as the error happened
public void AddError(ErrorRecord Error, object Target)
{
- Errors.Enqueue(new RSWorkerError(this, Error, Target));
+ ErrorRecord record = Error;
+ if (record.Exception is MethodInvocationException && Regex.IsMatch(record.Exception.Message, "^Exception calling \"InvokeGlobal\" with \"1\" argument\\(s\\)") && record.Exception.InnerException != null && record.Exception.InnerException is RuntimeException)
+ record = ((RuntimeException)record.Exception.InnerException).ErrorRecord;
+ Errors.Enqueue(new RSWorkerError(this, record, Target));
+ _LastError = Error;
}
+
+ ///
+ /// Add an error with a target to the errors queue
+ ///
+ /// The error that happened
+ /// The target that was being processed as the error happened
+ /// ID of the runspace that generated the error
+ public void AddError(ErrorRecord Error, object Target, Guid RunspaceID)
+ {
+ ErrorRecord record = Error;
+ if (record.Exception is MethodInvocationException && Regex.IsMatch(record.Exception.Message, "^Exception calling \"InvokeGlobal\" with \"1\" argument\\(s\\)") && record.Exception.InnerException != null && record.Exception.InnerException is RuntimeException)
+ record = ((RuntimeException)record.Exception.InnerException).ErrorRecord;
+ Errors.Enqueue(new RSWorkerError(this, record, Target, RunspaceID));
+ _LastError = Error;
+ }
}
}
diff --git a/library/PSFramework/Runspace/RSWorkerError.cs b/library/PSFramework/Runspace/RSWorkerError.cs
index 0e3c317e..fcb6ee63 100644
--- a/library/PSFramework/Runspace/RSWorkerError.cs
+++ b/library/PSFramework/Runspace/RSWorkerError.cs
@@ -43,11 +43,9 @@ public class RSWorkerError
/// The worker that failed
/// The error that happened
public RSWorkerError(RSWorker Worker, ErrorRecord Error)
+ :this(Worker, Error, null, System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId)
{
- this.Worker = Worker;
- this.Error = Error;
- Timestamp = DateTime.Now;
- Runspace = System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId;
+
}
///
@@ -57,11 +55,24 @@ public RSWorkerError(RSWorker Worker, ErrorRecord Error)
/// The error that happened
/// The current item where processing failed
public RSWorkerError(RSWorker Worker, ErrorRecord Error, object TargetObject)
+ :this(Worker, Error, TargetObject, System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId)
+ {
+ }
+
+ ///
+ /// Create a new error object for tracking purposes.
+ ///
+ /// The worker that failed
+ /// The error that happened
+ /// The current item where processing failed
+ /// The ID of the runspace that encountered the problem
+ public RSWorkerError(RSWorker Worker, ErrorRecord Error, object TargetObject, Guid RunspaceID)
{
+ this.TargetObject = TargetObject;
this.Worker = Worker;
this.Error = Error;
Timestamp = DateTime.Now;
- Runspace = System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId;
+ Runspace = RunspaceID;
}
///
diff --git a/library/PSFramework/Utility/DynamicContentCache.cs b/library/PSFramework/Utility/DynamicContentCache.cs
new file mode 100644
index 00000000..8576f21b
--- /dev/null
+++ b/library/PSFramework/Utility/DynamicContentCache.cs
@@ -0,0 +1,53 @@
+using PSFramework.Caching;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PSFramework.Utility
+{
+ ///
+ /// A dynamic content object that implements a dictionary
+ ///
+ public class DynamicContentCache : DynamicContentObject
+ {
+ ///
+ /// The value of the dynamic content object
+ ///
+ public new object Value
+ {
+ get { return _Cache; }
+ set
+ {
+ if (value == null)
+ _Cache = new CacheMemoryConcurrent();
+ else if ((value as CacheMemoryConcurrent) != null)
+ _Cache = value as CacheMemoryConcurrent;
+ else
+ throw new ArgumentException("Only accepts PSFramework Cache (Memory, Concurrent) objects. Specify a null value to reset or queue to add items!");
+ }
+ }
+ private CacheMemoryConcurrent _Cache = new CacheMemoryConcurrent();
+
+ ///
+ /// Creates a dynamic content object concurrent dictionary
+ ///
+ /// The name of the setting
+ /// The initial value of the object
+ public DynamicContentCache(string Name, object Value)
+ : base(Name, Value)
+ {
+
+ }
+
+ ///
+ /// Resets the stack by reestablishing an empty dictionary.
+ ///
+ public void Reset()
+ {
+ _Cache = new CacheMemoryConcurrent();
+ }
+ }
+}
diff --git a/library/PSFramework/Utility/DynamicContentObject.cs b/library/PSFramework/Utility/DynamicContentObject.cs
index 623fab03..acbf6ad4 100644
--- a/library/PSFramework/Utility/DynamicContentObject.cs
+++ b/library/PSFramework/Utility/DynamicContentObject.cs
@@ -1,4 +1,5 @@
-using System;
+using PSFramework.Caching;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -32,30 +33,37 @@ public static string[] List
/// The type of dynamic content object to create (if creatable)
public static void Set(string Name, object Value, DynamicContentObjectType Type = DynamicContentObjectType.Common)
{
- if (Values.ContainsKey(Name))
- Values[Name].Value = Value;
- else
+ lock (_SetLock)
{
- switch (Type)
+ if (Values.ContainsKey(Name))
+ Values[Name].Value = Value;
+ else
{
- case DynamicContentObjectType.Dictionary:
- Values[Name] = new DynamicContentDictionary(Name, Value);
- break;
- case DynamicContentObjectType.List:
- Values[Name] = new DynamicContentList(Name, Value);
- break;
- case DynamicContentObjectType.Queue:
- Values[Name] = new DynamicContentQueue(Name, Value);
- break;
- case DynamicContentObjectType.Stack:
- Values[Name] = new DynamicContentStack(Name, Value);
- break;
- default:
- Values[Name] = new DynamicContentObject(Name, Value);
- break;
+ switch (Type)
+ {
+ case DynamicContentObjectType.Dictionary:
+ Values[Name] = new DynamicContentDictionary(Name, Value);
+ break;
+ case DynamicContentObjectType.List:
+ Values[Name] = new DynamicContentList(Name, Value);
+ break;
+ case DynamicContentObjectType.Queue:
+ Values[Name] = new DynamicContentQueue(Name, Value);
+ break;
+ case DynamicContentObjectType.Stack:
+ Values[Name] = new DynamicContentStack(Name, Value);
+ break;
+ case DynamicContentObjectType.Cache:
+ Values[Name] = new DynamicContentCache(Name, Value);
+ break;
+ default:
+ Values[Name] = new DynamicContentObject(Name, Value);
+ break;
+ }
}
}
}
+ private static object _SetLock = 42;
///
/// Returns the Dynamic Content Object under the specified name
@@ -116,7 +124,7 @@ public void ConcurrentList(bool Reset = false)
}
///
- /// TUrns the value into a concurrent dictionary with case-insensitive string keys
+ /// Turns the value into a concurrent dictionary with case-insensitive string keys
///
public void ConcurrentDictionary(bool Reset = false)
{
@@ -126,6 +134,17 @@ public void ConcurrentDictionary(bool Reset = false)
Value = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase);
}
+ ///
+ /// Turns the value into a concurrent in-memory cache.
+ ///
+ public void ConcurrentCache(bool Reset = false)
+ {
+ if (Value == null || Reset)
+ Value = new CacheMemoryConcurrent();
+ else if (!(Value is CacheMemoryConcurrent))
+ Value = new CacheMemoryConcurrent();
+ }
+
///
/// General string representation of the value
///
diff --git a/library/PSFramework/Utility/DynamicContentObjectType.cs b/library/PSFramework/Utility/DynamicContentObjectType.cs
index 51fa0562..8266a123 100644
--- a/library/PSFramework/Utility/DynamicContentObjectType.cs
+++ b/library/PSFramework/Utility/DynamicContentObjectType.cs
@@ -28,6 +28,11 @@ public enum DynamicContentObjectType
///
/// A dictionary was requested
///
- Dictionary
+ Dictionary,
+
+ ///
+ /// A cache was requested
+ ///
+ Cache
}
}
diff --git a/notes.md b/notes.md
deleted file mode 100644
index 11a7831d..00000000
--- a/notes.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Notes
-
-> PSFCache
-
-+ Remove does not work
-
-> Tab Completion
-
-+ CacheDuration disables manually trained values
-+ Mixing results with trained values leads to duplicates