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; + } + } + } +}