Mutex.psm1
function Get-Mutex { <# .SYNOPSIS Get currently defined Mutexes. .DESCRIPTION Get currently defined Mutexes. Only returns mutexes owned and managed by this module. .PARAMETER Name Name of the mutex to retrieve. Supports wildcards, defaults to '*' .EXAMPLE PS C:\> Get-Mutex Return all mutexes. .EXAMPLE PS C:\> Get-Mutex -Name MyModule.LogFile Returns the mutex named "MyModule.LogFile" #> [CmdletBinding()] Param ( [string] $Name = '*' ) process { $script:mutexes.Values | Where-Object Name -like $Name } } function Invoke-MutexCommand { <# .SYNOPSIS Execute a scriptblock after acquiring a mutex lock and safely releasing it after. .DESCRIPTION Execute a scriptblock after acquiring a mutex lock and safely releasing it after. .PARAMETER Name Name of the mutex lock to acquire. .PARAMETER ErrorMessage The error message to generate when mutex lock acquisition fails. .PARAMETER ScriptBlock The scriptblock to execute after the lock is acquired. .PARAMETER Timeout How long to wait for mutex lock acquisition. This is incurred when another process in the same computer already holds the mutex of the same name. Defaults to 30s .PARAMETER ArgumentList Arguments to pass to the scriptblock being invoked. .PARAMETER Stream Return data as it arrives. This disables caching of data being returned by the scriptblock executed within the mutex lock. When used as part of a pipeline, output produced will pause the current command and pass the object down the pipeline directly. This enables memory optimization, as for example not all content of a large file needs to be stored in memory at the same time, but might cause conflicts with mutex locks, if multiple commands in the pipeline need distinct locks to be applied. .PARAMETER Temporary Remove all mutexes from management that did not exist before invokation. .EXAMPLE PS C:\> Invoke-MutexCommand "PS.Roles.$System.$Name" -ErrorMessage "Failed to acquire file access lock" -ScriptBlock $ScriptBlock Executes the provided scriptblock after locking execution behind the mutex named "PS.Roles.$System.$Name". If the lock fails, the error message "Failed to acquire file access lock" will be displayed and no action taken. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Name, [string] $ErrorMessage = 'Failed to acquire mutex lock', [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock, [TimeSpan] $Timeout = '00:00:30', [Parameter(ValueFromPipeline = $true)] $ArgumentList, [switch] $Stream, [switch] $Temporary ) process { $existedBefore = (Get-Mutex -Name $Name) -as [bool] if (-not (Lock-Mutex -Name $Name -Timeout $Timeout)) { Write-Error $ErrorMessage return } try { if ($Stream) { if ($PSBoundParameters.ContainsKey('ArgumentList')) { & $ScriptBlock $ArgumentList } else { & $ScriptBlock } Unlock-Mutex -Name $Name if ($Temporary -and -not $existedBefore) { Remove-Mutex -Name $Name } } else { # Store results and return after Mutex completes to avoid deadlock in pipeline scenarios if ($PSBoundParameters.ContainsKey('ArgumentList')) { $results = & $ScriptBlock $ArgumentList } else { $results = & $ScriptBlock } Unlock-Mutex -Name $Name if ($Temporary -and -not $existedBefore) { Remove-Mutex -Name $Name } $results } } catch { Unlock-Mutex -Name $Name if ($Temporary -and -not $existedBefore) { Remove-Mutex -Name $Name } $PSCmdlet.WriteError($_) } } } function Lock-Mutex { <# .SYNOPSIS Acquire a lock on a mutex. .DESCRIPTION Acquire a lock on a mutex. Implicitly calls New-Mutex if the mutex hasn't been taken under the management of the current process yet. .PARAMETER Name Name of the mutex to acquire a lock on. .PARAMETER Timeout How long to wait for acquiring the mutex, before giving up with an error. .EXAMPLE PS C:\> Lock-Mutex -Name MyModule.LogFile Acquire a lock on the mutex 'MyModule.LogFile' #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name, [timespan] $Timeout ) process { foreach ($mutexName in $Name) { if (-not $script:mutexes[$mutexName]) { New-Mutex -Name $mutexName } if (-not $Timeout) { $script:mutexes[$mutexName].Object.WaitOne() } else { try { $script:mutexes[$mutexName].Object.WaitOne($Timeout) } catch { Write-Error $_ continue } } $script:mutexes[$mutexName].Status = 'Locked' $script:mutexes[$mutexName].LockCount++ } } } function New-Mutex { <# .SYNOPSIS Create a new mutex managed by this module. .DESCRIPTION Create a new mutex managed by this module. The mutex is created in an unacquired state. Use Lock-Mutex to acquire the mutex. Note: Calling Lock-Mutex without first calling New-Mutex will implicitly call New-Mutex. .PARAMETER Name Name of the mutex to create. The name is what the system selects for when marshalling access: All mutexes with the same name block each other, across all processes on the current host. .PARAMETER Access Which set of permissions to apply to the mutex. - default: The system default permissions for mutexes. The creator and the system will have access. - anybody: Any authenticated person on the system can obtain mutex lock. - admins: Any process running with elevation can obtain mutex lock. .PARAMETER Security Provide a custom mutex security object, governing access to the mutex. .PARAMETER CaseSpecific Create the mutex with the specified name casing. By default, mutexes managed by this module are lowercased to guarantee case-insensitivity across all PowerShell executions. This however would potentially affect interoperability with other tools & languages, hence this parameter to enable casing fidelity at the cost of case sensitivity. Note: Even when enabling this, only one instance of name (compared WITHOUT case sensitivity) can be stored within this module! For example, the mutexes "Example" and "eXample" could not coexist within the Mutex PowerShell module, even though they are distinct from each other and even when using the -CaseSpecific parameter. .EXAMPLE PS C:\> New-Mutex -Name MyModule.LogFile Create a new, unlocked mutex named 'MyModule.LogFile' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'securitySet')] Param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(ParameterSetName = 'securitySet')] [ValidateSet('default', 'anybody', 'admins')] [string] $Access = $script:mutexDefaultAccess, [Parameter(ParameterSetName = 'object')] [System.Security.AccessControl.MutexSecurity] $Security = $script:mutexDefaultSecurity, [switch] $CaseSpecific ) process { $newName = $Name.ToLower() if ($CaseSpecific) { $newName = $Name } if ($script:mutexes[$newName]) { return } #region Generate Mutex object & Security if ($Access -ne "default") { $securityObject = [System.Security.AccessControl.MutexSecurity]::New() $securityObject.SetOwner([System.Security.Principal.WindowsIdentity]::GetCurrent().User) switch ($Access) { 'anybody' { $rules = @( [System.Security.AccessControl.MutexAccessRule]::new(([System.Security.Principal.SecurityIdentifier]'S-1-5-11'), 'FullControl', 'Allow') [System.Security.AccessControl.MutexAccessRule]::new(([System.Security.Principal.SecurityIdentifier]'S-1-5-18'), 'FullControl', 'Allow') [System.Security.AccessControl.MutexAccessRule]::new([System.Security.Principal.WindowsIdentity]::GetCurrent().User, 'FullControl', 'Allow') ) foreach ($rule in $rules) { $securityObject.AddAccessRule($rule) } } 'admins' { $rules = @( [System.Security.AccessControl.MutexAccessRule]::new(([System.Security.Principal.SecurityIdentifier]'S-1-5-32-544'), 'FullControl', 'Allow') [System.Security.AccessControl.MutexAccessRule]::new(([System.Security.Principal.SecurityIdentifier]'S-1-5-18'), 'FullControl', 'Allow') [System.Security.AccessControl.MutexAccessRule]::new([System.Security.Principal.WindowsIdentity]::GetCurrent().User, 'FullControl', 'Allow') ) foreach ($rule in $rules) { $securityObject.AddAccessRule($rule) } } } } if ($Security -and -not $PSBoundParameters.ContainsKey('Access')) { $securityObject = $Security } if ($securityObject) { if ($PSVersionTable.PSVersion.Major -gt 5) { $mutex = [System.Threading.Mutex]::new($false, $newName) [System.Threading.ThreadingAclExtensions]::SetAccessControl($mutex, $securityObject) } else { $mutex = [System.Threading.Mutex]::new($false, $newName, [ref]$null, $securityObject) } } else { $mutex = [System.Threading.Mutex]::new($false, $newName) } #endregion Generate Mutex object & Security $script:mutexes[$newName] = [PSCustomObject]@{ Name = $newName Status = "Open" Object = $mutex LockCount = 0 } } } function Remove-Mutex { <# .SYNOPSIS Removes a mutex from the list of available mutexes. .DESCRIPTION Removes a mutex from the list of available mutexes. Only affects mutexes owned and managed by this module. Will silently return on unknown mutexes, not throw an error. .PARAMETER Name Name of the mutex to remove. Must be an exact, case-insensitive match. .EXAMPLE PS C:\> Get-Mutex | Remove-Mutex Clear all mutex owned by the current runspace managed by this module. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name ) process { foreach ($mutexName in $Name) { if (-not $script:mutexes[$mutexName]) { continue } Unlock-Mutex -Name $mutexName $script:mutexes[$mutexName].Object.Dispose() $script:mutexes.Remove($mutexName) } } } function Set-MutexDefault { <# .SYNOPSIS Set default settings for mutex processing. .DESCRIPTION Set default settings for mutex processing. .PARAMETER Access The default access set when creating new mutexes. - default: The system default permissions for mutexes. The creator and the system will have access. - anybody: Any authenticated person on the system can obtain mutex lock. - admins: Any process running with elevation can obtain mutex lock. .PARAMETER Security A custom mutex security object, governing access to newly created mutexes if not otherwise specified. .EXAMPLE PS C:\> Set-MutexDefault -Access admins Set new mutexes to be - by default - accessible by all elevated processes #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [ValidateSet('default', 'anybody', 'admins')] [string] $Access, [AllowNull()] [System.Security.AccessControl.MutexSecurity] $Security ) process { if ($Access) { $script:mutexDefaultAccess = $Access } if ($PSBoundParameters.ContainsKey("Security")) { $script:mutexDefaultSecurity = $Security } } } function Unlock-Mutex { <# .SYNOPSIS Release the lock on a mutex you manage. .DESCRIPTION Release the lock on a mutex you manage. Will silently return if the mutex does not exist. .PARAMETER Name The name of the mutex to release the lock on. .EXAMPLE PS C:\> Unlock-Mutex -Name MyModule.LogFile Release the lock on the mutex 'MyModule.LogFile' .EXAMPLE PS C:\> Get-Mutex | Release-Mutex Release the lock on all mutexes managed. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string] $Name ) process { foreach ($mutexName in $Name) { if (-not $script:mutexes[$mutexName]) { return } $mutex = $script:mutexes[$mutexName] if ($mutex.Status -eq "Open" -and $mutex.LockCount -le 0) { return } try { $mutex.Object.ReleaseMutex() } catch { $PSCmdlet.WriteError($_) } $mutex.LockCount-- if ($mutex.LockCount -le 0) { $mutex.Status = 'Open' } } } } # Central list of all mutexes $script:mutexes = @{ } # Which permission should be used by default when creating a new mutex # Maps the the -Access parameter of New-Mutex $script:mutexDefaultAccess = 'default' # Which default security object to apply when creating a new mutex # Maps to the -Security parameter of New-Mutex $script:mutexDefaultSecurity = $null |