Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion PSFramework/PSFramework.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -76,6 +76,7 @@
'Get-PSFPipeline'
'Get-PSFResultCache'
'Get-PSFRunspace'
'Get-PSFRunspaceLock'
'Get-PSFRunspaceWorkflow'
'Get-PSFRunspaceWorker'
'Get-PSFScriptblock'
Expand Down
Binary file modified PSFramework/bin/PSFramework.dll
Binary file not shown.
Binary file modified PSFramework/bin/PSFramework.pdb
Binary file not shown.
92 changes: 92 additions & 0 deletions PSFramework/bin/PSFramework.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8573,6 +8573,98 @@
Purge all RBVs of datasets from all expired runspaces
</summary>
</member>
<member name="F:PSFramework.Runspace.RunspaceHost.Locks">
<summary>
List of runspace locks available across the process
</summary>
</member>
<member name="M:PSFramework.Runspace.RunspaceHost.GetRunspaceLock(System.String)">
<summary>
Retrieve a named runspace lock, creating it first if necessary.
</summary>
<param name="Name">Name of the lock to create.</param>
<returns>The lock object</returns>
</member>
<member name="T:PSFramework.Runspace.RunspaceLock">
<summary>
Offers a cross-runspace locking mechanism to PowerShell
</summary>
</member>
<member name="F:PSFramework.Runspace.RunspaceLock.Name">
<summary>
The name of the lock.
Mostly academic, used to keep track of locks across the process.
</summary>
</member>
<member name="F:PSFramework.Runspace.RunspaceLock._Owner">
<summary>
Who the current owner is.
</summary>
</member>
<member name="F:PSFramework.Runspace.RunspaceLock._LockedTime">
<summary>
When the lock was last taken.
</summary>
</member>
<member name="P:PSFramework.Runspace.RunspaceLock.Owner">
<summary>
The current effective owning runspace.
</summary>
</member>
<member name="P:PSFramework.Runspace.RunspaceLock.CurrentID">
<summary>
The current runspace from the perspective of the caller.
</summary>
</member>
<member name="F:PSFramework.Runspace.RunspaceLock.MaxLockTime">
<summary>
The maximum time the lock can be held. Infinite if 0 or less.
</summary>
</member>
<member name="F:PSFramework.Runspace.RunspaceLock.Lock">
<summary>
The actual lock used to marshal access.
</summary>
</member>
<member name="M:PSFramework.Runspace.RunspaceLock.#ctor">
<summary>
Creates an empty runspace lock.
</summary>
</member>
<member name="M:PSFramework.Runspace.RunspaceLock.#ctor(System.String)">
<summary>
Creates a named runspace lock.
Names are used for multiple runspaces to access the same lock, if retrieved through the system.
</summary>
<param name="Name">The name to assign to the lock.</param>
</member>
<member name="M:PSFramework.Runspace.RunspaceLock.#ctor(System.String,System.Int32)">
<summary>
Creates a named runspace lock with a maximum lock time.
</summary>
<param name="Name">The name for the runspace lock.</param>
<param name="MaxLockTime">The maximum time (in ms) that the lock can be held.</param>
</member>
<member name="M:PSFramework.Runspace.RunspaceLock.Open">
<summary>
Attempt to reserve the lock with a 30 seconds timeout.
If the timeout expires, an error will be thrown.
</summary>
</member>
<member name="M:PSFramework.Runspace.RunspaceLock.Open(PSFramework.Parameter.TimeSpanParameter)">
<summary>
Attempte to reserve the lock with a specified timeout.
If the timeout expires, an error will be thrown.
</summary>
<param name="Timeout">The timeout until we give up achieving the lock</param>
<exception cref="T:System.TimeoutException">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.</exception>
</member>
<member name="M:PSFramework.Runspace.RunspaceLock.Close">
<summary>
Release the current runspace's control over this lock.
No action, if the current runspace does not control the lock.
</summary>
</member>
<member name="T:PSFramework.Runspace.RunspaceResult">
<summary>
The result of a runspace task
Expand Down
5 changes: 5 additions & 0 deletions PSFramework/changelog.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
98 changes: 98 additions & 0 deletions PSFramework/functions/runspace/Get-PSFRunspaceLock.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 2 additions & 2 deletions PSFramework/functions/utility/New-PSFSupportPackage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
$TaskName,

[int]
$TaskRetentionCount = (Get-PSFConfigValue -FullName 'Utility.SupportPackage.TaskDumpLimit'),
$TaskRetentionCount = (Get-PSFConfigValue -FullName 'PSFramework.Utility.SupportPackage.TaskDumpLimit'),

[switch]
$Force,
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions library/PSFramework/PSFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
<Compile Include="Runspace\RunspaceBoundValueGeneric.cs" />
<Compile Include="Runspace\RunspaceContainer.cs" />
<Compile Include="Runspace\RunspaceHost.cs" />
<Compile Include="Runspace\RunspaceLock.cs" />
<Compile Include="Runspace\RunspaceResult.cs" />
<Compile Include="Runspace\RunspaceState.cs" />
<Compile Include="Runspace\RunspaceTask.cs" />
Expand Down
25 changes: 25 additions & 0 deletions library/PSFramework/Runspace/RunspaceHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static int RbvCleanupInterval
/// </summary>
public static ConcurrentDictionary<string, RunspaceContainer> Runspaces = new ConcurrentDictionary<string, RunspaceContainer>(StringComparer.InvariantCultureIgnoreCase);

#region Runspace Bound Values
/// <summary>
/// List of all runspace bound values in use
/// </summary>
Expand Down Expand Up @@ -84,5 +85,29 @@ public static void PurgeAllRunspaceBoundVariables()
foreach (RunspaceBoundValue value in _RunspaceBoundValues)
value.PurgeExpired();
}
#endregion Runspace Bound Values

#region Locks
/// <summary>
/// List of runspace locks available across the process
/// </summary>
private static ConcurrentDictionary<string, RunspaceLock> Locks = new ConcurrentDictionary<string, RunspaceLock>(StringComparer.InvariantCultureIgnoreCase);

private static object _Lock = 42;
/// <summary>
/// Retrieve a named runspace lock, creating it first if necessary.
/// </summary>
/// <param name="Name">Name of the lock to create.</param>
/// <returns>The lock object</returns>
public static RunspaceLock GetRunspaceLock(string Name)
{
lock (_Lock)
{
if (null == Locks[Name])
Locks[Name] = new RunspaceLock(Name);
}
return Locks[Name];
}
#endregion Locks
}
}
Loading