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, 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
    )

    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, ParameterSetName='Object')]
        [object]
        $Object,

        [Parameter(Mandatory=$true, ParameterSetName='Name')]
        [string]
        $Name,

        [Parameter()]
        [int]
        $Timeout = [System.Threading.Timeout]::Infinite,

        [switch]
        $CheckGlobal
    )

    # 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]) {
        throw 'Cannot lock value types'
    }

    # check if null and throw
    if ($null -eq $Object) {
        throw 'Cannot lock a null object'
    }

    # 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) {
        throw "Failed to acquire lock on object"
    }
}

<#
.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, ParameterSetName='Object')]
        [object]
        $Object,

        [Parameter(Mandatory=$true, ParameterSetName='Name')]
        [string]
        $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]) {
        throw 'Cannot unlock value types'
    }

    # check if null and throw
    if ($null -eq $Object) {
        throw 'Cannot unlock a null object'
    }

    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
{
    [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) {
        throw "A mutex with the following name already exists: $($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) {
        throw "No mutex found called '$($Name)'"
    }

    if (!$mutex.WaitOne($Timeout)) {
        throw "Failed to acquire mutex ownership. Mutex name: $($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) {
        throw "No mutex found called '$($Name)'"
    }

    $mutex.ReleaseMutex()
}

<#
.SYNOPSIS
Removes all Mutexes.
 
.DESCRIPTION
Removes all Mutexes.
 
.EXAMPLE
Clear-PodeMutexes
#>

function Clear-PodeMutexes
{
    [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) {
        throw "A semaphore with the following name already exists: $($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) {
        throw "No semaphore found called '$($Name)'"
    }

    if (!$semaphore.WaitOne($Timeout)) {
        throw "Failed to acquire semaphore ownership. Semaphore name: $($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) {
        throw "No semaphore found called '$($Name)'"
    }

    if ($ReleaseCount -lt 1) {
        $ReleaseCount = 1
    }

    $semaphore.Release($ReleaseCount)
}

<#
.SYNOPSIS
Removes all Semaphores.
 
.DESCRIPTION
Removes all Semaphores.
 
.EXAMPLE
Clear-PodeSemaphores
#>

function Clear-PodeSemaphores
{
    [CmdletBinding()]
    param()

    if (Test-PodeIsEmpty $PodeContext.Threading.Semaphores) {
        return
    }

    foreach ($name in $PodeContext.Threading.Semaphores.Keys.Clone()) {
        Remove-PodeSemaphore -Name $name
    }
}