diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2287a484..ee5df5b5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -16,7 +16,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 5dc863e9..75b79cb5 100644
--- a/PSFramework/PSFramework.psd1
+++ b/PSFramework/PSFramework.psd1
@@ -4,7 +4,7 @@
RootModule = 'PSFramework.psm1'
# Version number of this module.
- ModuleVersion = '1.13.416'
+ ModuleVersion = '1.13.418'
# ID used to uniquely identify this module
GUID = '8028b914-132b-431f-baa9-94a6952f21ff'
@@ -76,6 +76,7 @@
'Get-PSFPipeline'
'Get-PSFResultCache'
'Get-PSFRunspace'
+ 'Get-PSFRunspaceLock'
'Get-PSFRunspaceWorkflow'
'Get-PSFRunspaceWorker'
'Get-PSFScriptblock'
diff --git a/PSFramework/bin/PSFramework.dll b/PSFramework/bin/PSFramework.dll
index 746d783d..3dae4e19 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 a32d55e4..b1d5da10 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 d1d56fcf..aa3b2049 100644
--- a/PSFramework/bin/PSFramework.xml
+++ b/PSFramework/bin/PSFramework.xml
@@ -8573,6 +8573,98 @@
Purge all RBVs of datasets from all expired runspaces
+
+
+ List of runspace locks available across the process
+
+
+
+
+ Retrieve a named runspace lock, creating it first if necessary.
+
+ Name of the lock to create.
+ The lock object
+
+
+
+ Offers a cross-runspace locking mechanism to PowerShell
+
+
+
+
+ The name of the lock.
+ Mostly academic, used to keep track of locks across the process.
+
+
+
+
+ Who the current owner is.
+
+
+
+
+ When the lock was last taken.
+
+
+
+
+ The current effective owning runspace.
+
+
+
+
+ The current runspace from the perspective of the caller.
+
+
+
+
+ The maximum time the lock can be held. Infinite if 0 or less.
+
+
+
+
+ The actual lock used to marshal access.
+
+
+
+
+ Creates an empty runspace lock.
+
+
+
+
+ Creates a named runspace lock.
+ Names are used for multiple runspaces to access the same lock, if retrieved through the system.
+
+ The name to assign to the lock.
+
+
+
+ Creates a named runspace lock with a maximum lock time.
+
+ The name for the runspace lock.
+ The maximum time (in ms) that the lock can be held.
+
+
+
+ Attempt to reserve the lock with a 30 seconds timeout.
+ If the timeout expires, an error will be thrown.
+
+
+
+
+ Attempte to reserve the lock with a specified timeout.
+ If the timeout expires, an error will be thrown.
+
+ The timeout until we give up achieving the lock
+ If we have to wait longer than we are willing to wait. Might happen, if some other runspace takes too long to release the lock.
+
+
+
+ Release the current runspace's control over this lock.
+ No action, if the current runspace does not control the lock.
+
+
The result of a runspace task
diff --git a/PSFramework/changelog.md b/PSFramework/changelog.md
index cc1e7e4d..7b031839 100644
--- a/PSFramework/changelog.md
+++ b/PSFramework/changelog.md
@@ -1,5 +1,10 @@
# CHANGELOG
+## 1.13.418 (2025-11-17)
+
+- New: Get-PSFRunspaceLock - Create or retrieve a lock object for runspace use.
+- Fix: New-PSFSupportPackage - debug dumps in task mode were not correctly rotated.
+
## 1.13.416 (2025-10-22)
- Fix: Invoke-PSFRunspace - errors incorrectly show PSFramework error, rather than actual errors.
diff --git a/PSFramework/functions/runspace/Get-PSFRunspaceLock.ps1 b/PSFramework/functions/runspace/Get-PSFRunspaceLock.ps1
new file mode 100644
index 00000000..d0b8a1be
--- /dev/null
+++ b/PSFramework/functions/runspace/Get-PSFRunspaceLock.ps1
@@ -0,0 +1,98 @@
+function Get-PSFRunspaceLock {
+ <#
+ .SYNOPSIS
+ Create or retrieve a lock object for runspace use.
+
+ .DESCRIPTION
+ Create or retrieve a lock object for runspace use.
+ One of the fundamental features in multi-threading is the "lock":
+ A language feature in many programming languages, that helps marshal access to a resource from multiple threads.
+ The key goal here: Prevent concurrent access to a resources or process, that cannot be done concurrently.
+
+ PowerShell does not have such a feature.
+
+ This is where the RunspaceLock feature comes in:
+ This command generates an object, that can take over the role of the lock-feature in a PowerShell environment!
+
+ First create a lock object:
+ $lock = Get-PSFRunspaceLock -Name 'MyModule.Example'
+
+ Then you can obtain the lock calling the Open() method:
+ $lock.Open()
+ This will reserve the lock for the current runspace.
+ If another runspace tries to also call Open(), they will be forced to wait until the current runspace releases the lock.
+
+ Finally, to release the lock, call the Close() method:
+ $lock.Close()
+
+ Example implementation:
+ $lock = Get-PSFRunspaceLock -Name 'MyModule.ExchangeConnect'
+ $lock.Open()
+ try { Connect-IPPSSession }
+ finally { $lock.Close() }
+
+ This will guarantee, that only one runspace will call "Connect-IPPSSession" at a time, assuming all run this snippet.
+
+ .PARAMETER Name
+ The name of the runspace-lock.
+ No matter from which runspace, all instances using the same name utilize the same lock and can block each other.
+
+ .PARAMETER Timeout
+ How long a lock is valid for at most.
+ By default, a lock is valid for 30 seconds, after which it will expire and be released, in order to prevent permanent lockout / deadlock.
+ Increase this if more time is needed, setting this to 0 or less will remove the timeout.
+
+ .PARAMETER Unmanaged
+ By default, retrieving a lock with the same name will grant access to the exact same lock.
+ This makes it easy to use in casual runspace scenarios:
+ Simply call Get-PSFRunspaceLock in each runspace with the same name and you are good.
+ By making the lock unmanaged, you remove it from this system - the lock-object will not be tracked by PSFramework,
+ creating additional instances with the same name will NOT reference the same lock.
+ In return, you can safely pass in the lock object to whatever runspace you want with a guarantee to not conflict with anything else.
+ This parameter should generally not be needed for most scenarios.
+
+ .EXAMPLE
+ PS C:\> $lock = Get-PSFRunspaceLock -Name 'MyModule.ExchangeConnect'
+
+ Creates a new lock object named 'MyModule.ExchangeConnect'
+
+ .EXAMPLE
+ 1..20 | Invoke-PSFRunspace {
+ if (-not $global:connected) {
+ $lock = Get-PSFRunspaceLock -Name MyModule.ExchangeConnect
+ $lock.Open('5m')
+ try { Connect-IPPSSession }
+ finally { $lock.Close() }
+ $global:connected = $true
+ }
+ Get-Label
+ } -ThrottleLimit 4
+
+ In four background runspaces, it will savely connect to Purview and retrieve labels 20 times total, without getting into conflict.
+ #>
+ [OutputType([PSFramework.Runspace.RunspaceLock])]
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [string]
+ $Name,
+
+ [PsfTimeSpan]
+ $Timeout,
+
+ [switch]
+ $Unmanaged
+ )
+ process {
+ if ($Unmanaged) {
+ $lock = [PSFramework.Runspace.RunspaceLock]::new($Name)
+ }
+ else {
+ $lock = [PSFramework.Runspace.RunspaceHost]::GetRunspaceLock($Name)
+ }
+ if ($Timeout) {
+ $lock.MaxLockTime = $Timeout.Value.TotalMilliseconds
+ }
+ $lock
+ }
+}
\ No newline at end of file
diff --git a/PSFramework/functions/utility/New-PSFSupportPackage.ps1 b/PSFramework/functions/utility/New-PSFSupportPackage.ps1
index 1e67f416..cbf6aa9a 100644
--- a/PSFramework/functions/utility/New-PSFSupportPackage.ps1
+++ b/PSFramework/functions/utility/New-PSFSupportPackage.ps1
@@ -92,7 +92,7 @@
$TaskName,
[int]
- $TaskRetentionCount = (Get-PSFConfigValue -FullName 'Utility.SupportPackage.TaskDumpLimit'),
+ $TaskRetentionCount = (Get-PSFConfigValue -FullName 'PSFramework.Utility.SupportPackage.TaskDumpLimit'),
[switch]
$Force,
@@ -348,7 +348,7 @@
}
end {
if ($PSCmdlet.ParameterSetName -eq 'Task') {
- Get-ChildItem -Path $outputPath -Force -Filter *.cliDat |
+ Get-ChildItem -Path $outputPath -Force -Filter *.zip |
Microsoft.PowerShell.Utility\Sort-Object LastWriteTime -Descending |
Microsoft.PowerShell.Utility\Select-Object -Skip $TaskRetentionCount |
Remove-Item -Force
diff --git a/library/PSFramework/PSFramework.csproj b/library/PSFramework/PSFramework.csproj
index b356959b..0e58aaf3 100644
--- a/library/PSFramework/PSFramework.csproj
+++ b/library/PSFramework/PSFramework.csproj
@@ -218,6 +218,7 @@
+
diff --git a/library/PSFramework/Runspace/RunspaceHost.cs b/library/PSFramework/Runspace/RunspaceHost.cs
index 9ec6e4a1..a8510171 100644
--- a/library/PSFramework/Runspace/RunspaceHost.cs
+++ b/library/PSFramework/Runspace/RunspaceHost.cs
@@ -35,6 +35,7 @@ public static int RbvCleanupInterval
///
public static ConcurrentDictionary Runspaces = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase);
+ #region Runspace Bound Values
///
/// List of all runspace bound values in use
///
@@ -84,5 +85,29 @@ public static void PurgeAllRunspaceBoundVariables()
foreach (RunspaceBoundValue value in _RunspaceBoundValues)
value.PurgeExpired();
}
+ #endregion Runspace Bound Values
+
+ #region Locks
+ ///
+ /// List of runspace locks available across the process
+ ///
+ private static ConcurrentDictionary Locks = new ConcurrentDictionary(StringComparer.InvariantCultureIgnoreCase);
+
+ private static object _Lock = 42;
+ ///
+ /// Retrieve a named runspace lock, creating it first if necessary.
+ ///
+ /// Name of the lock to create.
+ /// The lock object
+ public static RunspaceLock GetRunspaceLock(string Name)
+ {
+ lock (_Lock)
+ {
+ if (null == Locks[Name])
+ Locks[Name] = new RunspaceLock(Name);
+ }
+ return Locks[Name];
+ }
+ #endregion Locks
}
}
diff --git a/library/PSFramework/Runspace/RunspaceLock.cs b/library/PSFramework/Runspace/RunspaceLock.cs
new file mode 100644
index 00000000..a9918bea
--- /dev/null
+++ b/library/PSFramework/Runspace/RunspaceLock.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Threading;
+using PSFramework.Parameter;
+
+namespace PSFramework.Runspace
+{
+ ///
+ /// Offers a cross-runspace locking mechanism to PowerShell
+ ///
+ public class RunspaceLock
+ {
+ ///
+ /// The name of the lock.
+ /// Mostly academic, used to keep track of locks across the process.
+ ///
+ public string Name = "";
+
+ ///
+ /// Who the current owner is.
+ ///
+ private Guid _Owner;
+
+ ///
+ /// When the lock was last taken.
+ ///
+ private DateTime _LockedTime;
+
+ ///
+ /// The current effective owning runspace.
+ ///
+ public Guid Owner
+ {
+ get
+ {
+ if (MaxLockTime > 0 && _LockedTime.AddMilliseconds(MaxLockTime) < DateTime.Now)
+ return Guid.Empty;
+ return _Owner;
+ }
+ private set
+ {
+ _LockedTime = DateTime.Now;
+ _Owner = value;
+ }
+ }
+
+ ///
+ /// The current runspace from the perspective of the caller.
+ ///
+ public Guid CurrentID { get => System.Management.Automation.Runspaces.Runspace.DefaultRunspace.InstanceId; }
+
+ ///
+ /// The maximum time the lock can be held. Infinite if 0 or less.
+ ///
+ public int MaxLockTime = 30000;
+
+ ///
+ /// The actual lock used to marshal access.
+ ///
+ private object Lock = 42;
+
+ ///
+ /// Creates an empty runspace lock.
+ ///
+ public RunspaceLock()
+ {
+
+ }
+
+ ///
+ /// Creates a named runspace lock.
+ /// Names are used for multiple runspaces to access the same lock, if retrieved through the system.
+ ///
+ /// The name to assign to the lock.
+ public RunspaceLock(string Name)
+ {
+ this.Name = Name;
+ }
+ ///
+ /// Creates a named runspace lock with a maximum lock time.
+ ///
+ /// The name for the runspace lock.
+ /// The maximum time (in ms) that the lock can be held.
+ public RunspaceLock(string Name, int MaxLockTime)
+ {
+ this.Name = Name;
+ this.MaxLockTime = MaxLockTime;
+ }
+
+ ///
+ /// Attempt to reserve the lock with a 30 seconds timeout.
+ /// If the timeout expires, an error will be thrown.
+ ///
+ public void Open()
+ {
+ Open(new TimeSpan(0, 0, 30));
+ }
+
+ ///
+ /// Attempte to reserve the lock with a specified timeout.
+ /// If the timeout expires, an error will be thrown.
+ ///
+ /// The timeout until we give up achieving the lock
+ /// If we have to wait longer than we are willing to wait. Might happen, if some other runspace takes too long to release the lock.
+ public void Open(TimeSpanParameter Timeout)
+ {
+ if (Owner == CurrentID)
+ return;
+
+ DateTime limit = DateTime.Now.Add(Timeout);
+ bool owned = false;
+
+ do
+ {
+ lock (Lock)
+ {
+ if (Owner == Guid.Empty)
+ {
+ Owner = CurrentID;
+ owned = true;
+ }
+ }
+ if (owned)
+ break;
+ if (DateTime.Now > limit)
+ throw new TimeoutException($"Failed to obtain lock '{Name}' within time limit!");
+ Thread.Sleep(50);
+ }
+ while (!owned);
+ }
+
+ ///
+ /// Release the current runspace's control over this lock.
+ /// No action, if the current runspace does not control the lock.
+ ///
+ public void Close()
+ {
+ if (Owner != CurrentID)
+ return;
+
+ lock (Lock)
+ {
+ Owner = Guid.Empty;
+ }
+ }
+ }
+}