Public/Threading.ps1
<#
.SYNOPSIS Places a temporary lock on an object, or Lockable, while a ScriptBlock is invoked. .DESCRIPTION Places a temporary lock on an object, or Lockable, while a ScriptBlock is invoked. .PARAMETER Object The Object, or Lockable, to lock. If no Object is supplied then the global lockable is used by default. .PARAMETER Name The Name of a Lockable object in Pode to lock, if no Name is supplied then the global lockable is used by default. .PARAMETER ScriptBlock The ScriptBlock to invoke. .PARAMETER Timeout If supplied, a number of milliseconds to timeout after if a lock cannot be acquired. (Default: Infinite) .PARAMETER Return If supplied, any values from the ScriptBlock will be returned. .PARAMETER CheckGlobal If supplied, will check the global Lockable object and wait until it's freed-up before locking the passed object. .EXAMPLE Lock-PodeObject -ScriptBlock { /* logic */ } .EXAMPLE Lock-PodeObject -Object $SomeArray -ScriptBlock { /* logic */ } .EXAMPLE Lock-PodeObject -Name 'LockName' -Timeout 5000 -ScriptBlock { /* logic */ } .EXAMPLE $result = (Lock-PodeObject -Return -Object $SomeArray -ScriptBlock { /* logic */ }) #> function Lock-PodeObject { [CmdletBinding(DefaultParameterSetName = 'Object')] [OutputType([object])] param( [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Object')] [object] $Object, [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] $Name, [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock, [Parameter()] [int] $Timeout = [System.Threading.Timeout]::Infinite, [switch] $Return, [switch] $CheckGlobal ) begin { $pipelineItemCount = 0 } process { $pipelineItemCount++ } end { if ($pipelineItemCount -gt 1) { throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } try { if ([string]::IsNullOrEmpty($Name)) { Enter-PodeLockable -Object $Object -Timeout $Timeout -CheckGlobal:$CheckGlobal } else { Enter-PodeLockable -Name $Name -Timeout $Timeout -CheckGlobal:$CheckGlobal } if ($null -ne $ScriptBlock) { Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return } } catch { $_ | Write-PodeErrorLog throw $_.Exception } finally { if ([string]::IsNullOrEmpty($Name)) { Exit-PodeLockable -Object $Object } else { Exit-PodeLockable -Name $Name } } } } <# .SYNOPSIS Creates a new custom Lockable object. .DESCRIPTION Creates a new custom Lockable object for use with Lock-PodeObject, and Enter/Exit-PodeLockable. .PARAMETER Name The Name of the Lockable object. .EXAMPLE New-PodeLockable -Name 'Lock1' #> function New-PodeLockable { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) if (Test-PodeLockable -Name $Name) { return } $PodeContext.Threading.Lockables.Custom[$Name] = [hashtable]::Synchronized(@{}) } <# .SYNOPSIS Removes a custom Lockable object. .DESCRIPTION Removes a custom Lockable object. .PARAMETER Name The Name of the Lockable object to remove. .EXAMPLE Remove-PodeLockable -Name 'Lock1' #> function Remove-PodeLockable { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) if (Test-PodeLockable -Name $Name) { $PodeContext.Threading.Lockables.Custom.Remove($Name) } } <# .SYNOPSIS Get a custom Lockable object. .DESCRIPTION Get a custom Lockable object for use with Lock-PodeObject, and Enter/Exit-PodeLockable. .PARAMETER Name The Name of the Lockable object. .EXAMPLE Get-PodeLockable -Name 'Lock1' | Lock-PodeObject -ScriptBlock {} #> function Get-PodeLockable { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) return $PodeContext.Threading.Lockables.Custom[$Name] } <# .SYNOPSIS Test if a custom Lockable object exists. .DESCRIPTION Test if a custom Lockable object exists. .PARAMETER Name The Name of the Lockable object. .EXAMPLE Test-PodeLockable -Name 'Lock1' #> function Test-PodeLockable { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) return $PodeContext.Threading.Lockables.Custom.ContainsKey($Name) } <# .SYNOPSIS Place a lock on an object or Lockable. .DESCRIPTION Place a lock on an object or Lockable. This should eventually be followed by a call to Exit-PodeLockable. .PARAMETER Object The Object, or Lockable, to lock. If no Object is supplied then the global lockable is used by default. .PARAMETER Name The Name of a Lockable object in Pode to lock, if no Name is supplied then the global lockable is used by default. .PARAMETER Timeout If supplied, a number of milliseconds to timeout after if a lock cannot be acquired. (Default: Infinite) .PARAMETER CheckGlobal If supplied, will check the global Lockable object and wait until it's freed-up before locking the passed object. .EXAMPLE Enter-PodeLockable -Object $SomeArray .EXAMPLE Enter-PodeLockable -Name 'LockName' -Timeout 5000 #> function Enter-PodeLockable { [CmdletBinding(DefaultParameterSetName = 'Object')] param( [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Object')] [object] $Object, [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] $Name, [Parameter()] [int] $Timeout = [System.Threading.Timeout]::Infinite, [switch] $CheckGlobal ) begin { $pipelineItemCount = 0 } process { $pipelineItemCount++ } end { if ($pipelineItemCount -gt 1) { throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } # get object by name if set if (![string]::IsNullOrEmpty($Name)) { $Object = Get-PodeLockable -Name $Name } # if object is null, default to global if ($null -eq $Object) { $Object = $PodeContext.Threading.Lockables.Global } # check if value type and throw if ($Object -is [valuetype]) { # Cannot lock a [ValueType] throw ($PodeLocale.cannotLockValueTypeExceptionMessage) } # check if null and throw if ($null -eq $Object) { # Cannot lock an object that is null throw ($PodeLocale.cannotLockNullObjectExceptionMessage) } # check if the global lockable is locked if ($CheckGlobal) { Lock-PodeObject -Object $PodeContext.Threading.Lockables.Global -ScriptBlock {} -Timeout $Timeout } # attempt to acquire lock $locked = $false [System.Threading.Monitor]::TryEnter($Object.SyncRoot, $Timeout, [ref]$locked) if (!$locked) { # Failed to acquire a lock on the object throw ($PodeLocale.failedToAcquireLockExceptionMessage) } } } <# .SYNOPSIS Remove a lock from an object or Lockable. .DESCRIPTION Remove a lock from an object or Lockable, that was originally locked via Enter-PodeLockable. .PARAMETER Object The Object, or Lockable, to unlock. If no Object is supplied then the global lockable is used by default. .PARAMETER Name The Name of a Lockable object in Pode to unlock, if no Name is supplied then the global lockable is used by default. .EXAMPLE Exit-PodeLockable -Object $SomeArray .EXAMPLE Exit-PodeLockable -Name 'LockName' #> function Exit-PodeLockable { [CmdletBinding(DefaultParameterSetName = 'Object')] param( [Parameter(ValueFromPipeline = $true, Position = 0, ParameterSetName = 'Object')] [object] $Object, [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] $Name ) begin { $pipelineItemCount = 0 } process { $pipelineItemCount++ } end { if ($pipelineItemCount -gt 1) { throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } # get object by name if set if (![string]::IsNullOrEmpty($Name)) { $Object = Get-PodeLockable -Name $Name } # if object is null, default to global if ($null -eq $Object) { $Object = $PodeContext.Threading.Lockables.Global } # check if value type and throw if ($Object -is [valuetype]) { # Cannot unlock a [ValueType] throw ($PodeLocale.cannotUnlockValueTypeExceptionMessage) } # check if null and throw if ($null -eq $Object) { # Cannot unlock an object that is null throw ($PodeLocale.cannotUnlockNullObjectExceptionMessage) } if ([System.Threading.Monitor]::IsEntered($Object.SyncRoot)) { [System.Threading.Monitor]::Pulse($Object.SyncRoot) [System.Threading.Monitor]::Exit($Object.SyncRoot) } } } <# .SYNOPSIS Remove all Lockables. .DESCRIPTION Remove all Lockables. .EXAMPLE Clear-PodeLockables #> function Clear-PodeLockables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() if (Test-PodeIsEmpty $PodeContext.Threading.Lockables.Custom) { return } foreach ($name in $PodeContext.Threading.Lockables.Custom.Keys.Clone()) { Remove-PodeLockable -Name $name } } <# .SYNOPSIS Create a new Mutex. .DESCRIPTION Create a new Mutex. .PARAMETER Name The Name of the Mutex. .PARAMETER Scope The Scope of the Mutex, can be either Self, Local, or Global. (Default: Self) Self: The current process, or child processes. Local: All processes for the current login session on Windows, or the the same as Self on Unix. Global: All processes on the system, across every session. .EXAMPLE New-PodeMutex -Name 'SelfMutex' .EXAMPLE New-PodeMutex -Name 'LocalMutex' -Scope Local .EXAMPLE New-PodeMutex -Name 'GlobalMutex' -Scope Global #> function New-PodeMutex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [ValidateSet('Self', 'Local', 'Global')] [string] $Scope = 'Self' ) if (Test-PodeMutex -Name $Name) { # A mutex with the following name already exists throw ($PodeLocale.mutexAlreadyExistsExceptionMessage -f $Name) } $mutex = $null switch ($Scope.ToLowerInvariant()) { 'self' { $mutex = [System.Threading.Mutex]::new($false) } 'local' { $mutex = [System.Threading.Mutex]::new($false, "Local\$($Name)") } 'global' { $mutex = [System.Threading.Mutex]::new($false, "Global\$($Name)") } } $PodeContext.Threading.Mutexes[$Name] = $mutex } <# .SYNOPSIS Test if a Mutex exists. .DESCRIPTION Test if a Mutex exists. .PARAMETER Name The Name of the Mutex. .EXAMPLE Test-PodeMutex -Name 'LocalMutex' #> function Test-PodeMutex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) return $PodeContext.Threading.Mutexes.ContainsKey($Name) } <# .SYNOPSIS Get a Mutex. .DESCRIPTION Get a Mutex. .PARAMETER Name The Name of the Mutex. .EXAMPLE $mutex = Get-PodeMutex -Name 'SelfMutex' #> function Get-PodeMutex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) return $PodeContext.Threading.Mutexes[$Name] } <# .SYNOPSIS Remove a Mutex. .DESCRIPTION Remove a Mutex. .PARAMETER Name The Name of the Mutex. .EXAMPLE Remove-PodeMutex -Name 'GlobalMutex' #> function Remove-PodeMutex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) if (Test-PodeMutex -Name $Name) { $PodeContext.Threading.Mutexes[$Name].Dispose() $PodeContext.Threading.Mutexes.Remove($Name) } } <# .SYNOPSIS Places a temporary hold on a Mutex, invokes a ScriptBlock, then releases the Mutex. .DESCRIPTION Places a temporary hold on a Mutex, invokes a ScriptBlock, then releases the Mutex. .PARAMETER Name The Name of the Mutex. .PARAMETER ScriptBlock The ScriptBlock to invoke. .PARAMETER Timeout If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Mutex. (Default: Infinite) .PARAMETER Return If supplied, any values from the ScriptBlock will be returned. .EXAMPLE Use-PodeMutex -Name 'SelfMutex' -Timeout 5000 -ScriptBlock {} .EXAMPLE $result = Use-PodeMutex -Name 'LocalMutex' -Return -ScriptBlock {} #> function Use-PodeMutex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock, [Parameter()] [int] $Timeout = [System.Threading.Timeout]::Infinite, [switch] $Return ) try { $acquired = $false Enter-PodeMutex -Name $Name -Timeout $Timeout $acquired = $true Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return } catch { $_ | Write-PodeErrorLog throw $_.Exception } finally { if ($acquired) { Exit-PodeMutex -Name $Name } } } <# .SYNOPSIS Acquires a hold on a Mutex. .DESCRIPTION Acquires a hold on a Mutex. This should eventually by followed by a call to Exit-PodeMutex. .PARAMETER Name The Name of the Mutex. .PARAMETER Timeout If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Mutex. (Default: Infinite) .EXAMPLE Enter-PodeMutex -Name 'SelfMutex' -Timeout 5000 #> function Enter-PodeMutex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [int] $Timeout = [System.Threading.Timeout]::Infinite ) $mutex = Get-PodeMutex -Name $Name if ($null -eq $mutex) { # No mutex found called 'Name' throw ($PodeLocale.noMutexFoundExceptionMessage -f $Name) } if (!$mutex.WaitOne($Timeout)) { # Failed to acquire mutex ownership. Mutex name: Name throw ($PodeLocale.failedToAcquireMutexOwnershipExceptionMessage -f $Name) } } <# .SYNOPSIS Release the hold on a Mutex. .DESCRIPTION Release the hold on a Mutex, that was originally acquired by Enter-PodeMutex. .PARAMETER Name The Name of the Mutex. .EXAMPLE Exit-PodeMutex -Name 'SelfMutex' #> function Exit-PodeMutex { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) $mutex = Get-PodeMutex -Name $Name if ($null -eq $mutex) { # No mutex found called 'Name' throw ($PodeLocale.noMutexFoundExceptionMessage -f $Name) } $mutex.ReleaseMutex() } <# .SYNOPSIS Removes all Mutexes. .DESCRIPTION Removes all Mutexes. .EXAMPLE Clear-PodeMutexes #> function Clear-PodeMutexes { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() if (Test-PodeIsEmpty $PodeContext.Threading.Mutexes) { return } foreach ($name in $PodeContext.Threading.Mutexes.Keys.Clone()) { Remove-PodeMutex -Name $name } } <# .SYNOPSIS Create a new Semaphore. .DESCRIPTION Create a new Semaphore. .PARAMETER Name The Name of the Semaphore. .PARAMETER Count The number of threads to allow a hold on the Semaphore. (Default: 1) .PARAMETER Scope The Scope of the Semaphore, can be either Self, Local, or Global. (Default: Self) Self: The current process, or child processes. Local: All processes for the current login session on Windows, or the the same as Self on Unix. Global: All processes on the system, across every session. .EXAMPLE New-PodeSemaphore -Name 'SelfSemaphore' .EXAMPLE New-PodeSemaphore -Name 'LocalSemaphore' -Scope Local .EXAMPLE New-PodeSemaphore -Name 'GlobalSemaphore' -Count 3 -Scope Global #> function New-PodeSemaphore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [int] $Count = 1, [Parameter()] [ValidateSet('Self', 'Local', 'Global')] [string] $Scope = 'Self' ) if (Test-PodeSemaphore -Name $Name) { # A semaphore with the following name already exists throw ($PodeLocale.semaphoreAlreadyExistsExceptionMessage -f $Name) } if ($Count -le 0) { $Count = 1 } $semaphore = $null switch ($Scope.ToLowerInvariant()) { 'self' { $semaphore = [System.Threading.Semaphore]::new($Count, $Count) } 'local' { $semaphore = [System.Threading.Semaphore]::new($Count, $Count, "Local\$($Name)") } 'global' { $semaphore = [System.Threading.Semaphore]::new($Count, $Count, "Global\$($Name)") } } $PodeContext.Threading.Semaphores[$Name] = $semaphore } <# .SYNOPSIS Test if a Semaphore exists. .DESCRIPTION Test if a Semaphore exists. .PARAMETER Name The Name of the Semaphore. .EXAMPLE Test-PodeSemaphore -Name 'LocalSemaphore' #> function Test-PodeSemaphore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) return $PodeContext.Threading.Semaphores.ContainsKey($Name) } <# .SYNOPSIS Get a Semaphore. .DESCRIPTION Get a Semaphore. .PARAMETER Name The Name of the Semaphore. .EXAMPLE $semaphore = Get-PodeSemaphore -Name 'SelfSemaphore' #> function Get-PodeSemaphore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) return $PodeContext.Threading.Semaphores[$Name] } <# .SYNOPSIS Remove a Semaphore. .DESCRIPTION Remove a Semaphore. .PARAMETER Name The Name of the Semaphore. .EXAMPLE Remove-PodeSemaphore -Name 'GlobalSemaphore' #> function Remove-PodeSemaphore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) if (Test-PodeSemaphore -Name $Name) { $PodeContext.Threading.Semaphores[$Name].Dispose() $PodeContext.Threading.Semaphores.Remove($Name) } } <# .SYNOPSIS Places a temporary hold on a Semaphore, invokes a ScriptBlock, then releases the Semaphore. .DESCRIPTION Places a temporary hold on a Semaphore, invokes a ScriptBlock, then releases the Semaphore. .PARAMETER Name The Name of the Semaphore. .PARAMETER ScriptBlock The ScriptBlock to invoke. .PARAMETER Timeout If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Semaphore. (Default: Infinite) .PARAMETER Return If supplied, any values from the ScriptBlock will be returned. .EXAMPLE Use-PodeSemaphore -Name 'SelfSemaphore' -Timeout 5000 -ScriptBlock {} .EXAMPLE $result = Use-PodeSemaphore -Name 'LocalSemaphore' -Return -ScriptBlock {} #> function Use-PodeSemaphore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock, [Parameter()] [int] $Timeout = [System.Threading.Timeout]::Infinite, [switch] $Return ) try { $acquired = $false Enter-PodeSemaphore -Name $Name -Timeout $Timeout $acquired = $true Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -NoNewClosure -Return:$Return } catch { $_ | Write-PodeErrorLog throw $_.Exception } finally { if ($acquired) { Exit-PodeSemaphore -Name $Name } } } <# .SYNOPSIS Acquires a hold on a Semaphore. .DESCRIPTION Acquires a hold on a Semaphore. This should eventually by followed by a call to Exit-PodeSemaphore. .PARAMETER Name The Name of the Semaphore. .PARAMETER Timeout If supplied, a number of milliseconds to timeout after if a hold cannot be acquired on the Semaphore. (Default: Infinite) .EXAMPLE Enter-PodeSemaphore -Name 'SelfSemaphore' -Timeout 5000 #> function Enter-PodeSemaphore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [int] $Timeout = [System.Threading.Timeout]::Infinite ) $semaphore = Get-PodeSemaphore -Name $Name if ($null -eq $semaphore) { # No semaphore found called 'Name' throw ($PodeLocale.noSemaphoreFoundExceptionMessage -f $Name) } if (!$semaphore.WaitOne($Timeout)) { # Failed to acquire semaphore ownership. Semaphore name: Name throw ($PodeLocale.failedToAcquireSemaphoreOwnershipExceptionMessage -f $Name) } } <# .SYNOPSIS Release the hold on a Semaphore. .DESCRIPTION Release the hold on a Semaphore, that was originally acquired by Enter-PodeSemaphore. .PARAMETER Name The Name of the Semaphore. .PARAMETER ReleaseCount The number of releases to release in one go. (Default: 1) .EXAMPLE Exit-PodeSemaphore -Name 'SelfSemaphore' #> function Exit-PodeSemaphore { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [int] $ReleaseCount = 1 ) $semaphore = Get-PodeSemaphore -Name $Name if ($null -eq $semaphore) { # No semaphore found called 'Name' throw ($PodeLocale.noSemaphoreFoundExceptionMessage -f $Name) } if ($ReleaseCount -lt 1) { $ReleaseCount = 1 } $semaphore.Release($ReleaseCount) } <# .SYNOPSIS Removes all Semaphores. .DESCRIPTION Removes all Semaphores. .EXAMPLE Clear-PodeSemaphores #> function Clear-PodeSemaphores { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param() if (Test-PodeIsEmpty $PodeContext.Threading.Semaphores) { return } foreach ($name in $PodeContext.Threading.Semaphores.Keys.Clone()) { Remove-PodeSemaphore -Name $name } } |