MetaNull.MessageQueue.psm1

# Module Constants


$User = [Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = [Security.Principal.WindowsPrincipal]::new($User)
$Role = [Security.Principal.WindowsBuiltInRole]::Administrator
if($Principal.IsInRole($Role)) {
    # Current use is Administrator
    $PSDriveRoot = 'HKLM:\SOFTWARE\MetaNull\PowerShell\MetaNull.MessageQueue'
} else {
    # Current user is not Administrator
    $PSDriveRoot = 'HKCU:\SOFTWARE\MetaNull\PowerShell\MetaNull.MessageQueue'
}

if(-not (Test-Path $PSDriveRoot)) {
    New-Item -Path $PSDriveRoot -Force | Out-Null
}

New-Variable MetaNull -Scope script -Value @{
    MessageQueue = @{
        PSDriveRoot = $PSDriveRoot
        LockMessageQueue = New-Object Object
        MutexMessageQueue = New-Object System.Threading.Mutex($false, 'MetaNull.MessageQueue')
        Drive = New-PSDrive -Name 'MetaNull' -Scope Script -PSProvider Registry -Root $PSDriveRoot
    }
}

if(-not (Test-Path MetaNull:\MessageStore)) {
    # Create the MessageStore directory in the registry
    # This is where the message details will be stored
    New-Item -Path MetaNull:\MessageStore -Force | Out-Null
}
if(-not (Test-Path MetaNull:\MessageQueue)) {
    # Create the MessageQueue directory in the registry
    # This is where the message queues will be stored
    New-Item -Path MetaNull:\MessageQueue -Force | Out-Null
}
Function Clear-MessageQueue {
<#
.SYNOPSIS
    Clears the message queue for a specified queue name.
.DESCRIPTION
    This function clears the message queue for a specified queue name.
.PARAMETER MessageQueueId
    The ID of the message queue to be cleared.
.EXAMPLE
    Clear-MessageQueue -Id '12345678-1234-1234-1234-123456789012'
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Medium')]
[OutputType([void])]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
    [guid]$MessageQueueId
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null
        Get-Item "MetaNull:\MessageQueue\$MessageQueueId" | Get-ChildItem | Remove-Item | Out-Null
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Find-MessageQueue {
<#
.SYNOPSIS
    Find a message queue by Name
.DESCRIPTION
    Find a message queue by Name
.PARAMETER Name
    The name of the message queue
.EXAMPLE
    Find-MessageQueue -Name 'MyQueue'
.EXAMPLE
    Find-MessageQueue -Name 'My*'
.OUTPUTS
    [guid] True if the message queue exists, otherwise false.
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')]
[OutputType([guid],[Object[]])]
param(
    [Parameter(Mandatory = $false, ValueFromPipeline, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueName @args} )]
    [SupportsWildcards()]
    [string]$Name = '*'
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        if(-not $Name) {
            $Name = '*'
        }
        Get-Item "MetaNull:\MessageQueue" | Get-ChildItem | Where-Object {
            $_ | Get-ItemProperty | Select-Object -ExpandProperty Name | Where-Object { 
                $_ -like $Name
            }
        } | Select-Object -ExpandProperty PSChildName | ForEach-Object {
            [guid]::new($_)
        }
    } finally {
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Get-Message {
<#
.SYNOPSIS
    Get the messages in a message queue.
.DESCRIPTION
    Get the messages in a message queue.
.PARAMETER MessageQueueId
    The ID of the message queue to be retrieved.
.EXAMPLE
    Get-MessageQueue -Id '12345678-1234-1234-1234-123456789012'
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')]
[OutputType([Object],[Object[]])]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
    [guid]$MessageQueueId
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null

        # Get the messages in the queue
        $Messages = Get-Item -Path "MetaNull:\MessageQueue\$MessageQueueId" | Get-ChildItem | Get-ItemProperty
        $Messages | Sort-Object -Property Index | Foreach-Object {
            $Message = $_
            Get-Item -Path "MetaNull:\MessageStore\$($Message.MessageId)" | Get-ItemProperty | Select-Object -Property @(
                @{Name = 'MessageQueueId'; Expression = { $MessageQueueId }}
                @{Name = 'MessageId'; Expression = { [guid]($Message.MessageId) }}
                @{Name = 'Index'; Expression = { [int]$Message.Index }}
                @{Name = 'Label'; Expression = { $_.Label }}
                @{Name = 'Date'; Expression = { [datetime]($_.Date | ConvertFrom-Json) }}
                @{Name = 'MetaData'; Expression = { $_.MetaData | ConvertFrom-Json }}
            ) | Write-Output
        }
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Get-MessageQueue {
<#
.SYNOPSIS
    Get a message queue.
.DESCRIPTION
    This function gets a message queue.
.PARAMETER Id
    The ID of the message queue to be retrieved. This is a mandatory parameter and must be provided.
.EXAMPLE
    Get-MessageQueue -Id '12345678-1234-1234-1234-123456789012'
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')]
[OutputType([Object],[Object[]])]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
    [guid]$MessageQueueId
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null
        # Get the message queue and its properties
        Get-ItemProperty -Path "MetaNull:\MessageQueue\$MessageQueueId" | Select-Object -ExcludeProperty PS*
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function New-MessageQueue {
<#
.SYNOPSIS
    Create a new message queue.
.DESCRIPTION
    This function creates a new message queue.
.PARAMETER Name
    The name of the message queue to be created.
.PARAMETER MaximumSize
    The maximum size of the message queue.
    This is an optional parameter and defaults to 100 messages.
.PARAMETER MessageRetentionPeriod
    The message retention period in days.
    This is an optional parameter and defaults to 7 days.
.EXAMPLE
    New-MessageQueue -Name 'MyQueue' -MaximumSize 100 -MessageRetentionPeriod 7
.OUTPUTS
    [guid] The ID of the newly created message queue.
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Medium')]
[OutputType([guid])]
param(
    [Parameter(Mandatory, Position = 0)]
    [string]$Name,

    [Parameter(Mandatory = $false, Position = 1)]
    [ValidateRange(1, 10000)]
    [int]$MaximumSize = 100,
    
    [Parameter(Mandatory = $false, Position = 2)]
    [ValidateRange(1, 365)]
    [int]$MessageRetentionPeriod = 7
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null

        # Check if the queue already exists
        if(Find-MessageQueue -Name $Name) {
            throw "A MessageQueue with name $Name already exists."
        }
        
        # Create the message queue
        $MessageQueueId = New-Guid
        $Item = New-Item -Path "MetaNull:\MessageQueue\$MessageQueueId"
        $Item | Set-ItemProperty -Name 'MessageQueueId' -Value $MessageQueueId
        $Item | Set-ItemProperty -Name 'Name' -Value $Name
        $Item | Set-ItemProperty -Name 'MaximumSize' -Value $MaximumSize
        $Item | Set-ItemProperty -Name 'MessageRetentionPeriod' -Value $MessageRetentionPeriod

        return $MessageQueueId
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Optimize-MessageQueues {
<#
.SYNOPSIS
    Clears the message queue from outdated or excess messages.
.DESCRIPTION
    This function clears the message queue from outdated or excess messages.
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Medium')]
[OutputType([void])]
param()
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null

        $CurrentDate = (Get-Date)
        $Queues = Get-Item "MetaNull:\MessageQueue" | Get-ChildItem
        $Queues | Foreach-Object {
            Write-Verbose "Processing MessageQueue $($_.PSChildName)"
            $Properties = $_ | Get-ItemProperty

            # Remove Excess Messages
            if($Properties.MaximumSize -gt 0) {
                $Children = $_ | Get-ChildItem | Sort-Object { $_ | Get-ItemProperty | Select-Object -ExpandProperty Index }
                if($Children.Count -gt $Properties.MaximumSize) {
                    $ExcessMessages = $Children.Count - $Properties.MaximumSize
                    Write-Verbose "Removing $ExcessMessages excess message(s) from MessageQueue"
                    $Children | Select-Object -First $ExcessMessages | Remove-Item
                }
            }
            # Remove Outdated Messages
            if($Properties.MessageRetentionPeriod -gt 0) {
                $DateConstraint = $CurrentDate.AddDays(-$Properties.MessageRetentionPeriod)
                $Children = $_ | Get-ChildItem | Where-Object {
                    $DateConstraint -gt ([datetime]($_ | Get-ItemProperty | Select-Object -ExpandProperty Date | ConvertFrom-Json))
                }
                if($Children.Count -gt 0) {
                    $ExcessMessages = $Children.Count
                    Write-Verbose "Removing $ExcessMessages outdated message(s) from MessageQueue"
                    $Children | Remove-Item
                }
            }
        }
        
        # Remove leftover messages from the MessageStore
        $MessageIdList = Get-Item "MetaNull:\MessageQueue" | Get-ChildItem | Get-ChildItem | Get-ItemProperty | Select-Object -ExpandProperty MessageId | Select-Object -unique
        $Children = Get-Item "MetaNull:\MessageStore" | Get-ChildItem | Where-Object {
            $_.PSChildName -notin $MessageIdList
        }
        if($Children.Count -gt 0) {
            $ExcessMessages = $Children.Count
            Write-Verbose "Removing $ExcessMessages unlinked message(s) from MessageStore"
            $Children | Remove-Item
        }
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Pop-Message {
<#
.SYNOPSIS
    Get and remove the oldest message in a message queue.
.DESCRIPTION
    Get and remove the oldest message in a message queue.
.PARAMETER MessageQueueId
    The ID of the message queue(s) where message should be added to.
.EXAMPLE
    Pop-Message -Id '12345678-1234-1234-1234-123456789012'
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')]
[OutputType([PSCustomObject])]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
    [guid[]]$MessageQueueId
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null

        # Get the one oldest message in the queue
        $Messages = Get-Item -Path "MetaNull:\MessageQueue\$MessageQueueId" | Get-ChildItem | Get-ItemProperty
        $MessageQueueItem = $Messages | Sort-Object -Property Index | Select-Object -First 1 | Foreach-Object {
            $Message = $_
            Get-Item -Path "MetaNull:\MessageStore\$($Message.MessageId)" | Get-ItemProperty | Select-Object -Property @(
                @{Name = 'MessageQueueId'; Expression = { $MessageQueueId }}
                @{Name = 'MessageId'; Expression = { [guid]($Message.MessageId) }}
                @{Name = 'Index'; Expression = { [int]$Message.Index }}
                @{Name = 'Label'; Expression = { $_.Label }}
                @{Name = 'Date'; Expression = { [datetime]($_.Date | ConvertFrom-Json) }}
                @{Name = 'MetaData'; Expression = { $_.MetaData | ConvertFrom-Json }}
            )
        }

        # Remove the message from the message queue (but not from the message store)
        Get-Item -Path "MetaNull:\MessageQueue\$MessageQueueId" | Get-ChildItem | Where-Object {
            $_.PSChildName -eq $MessageQueueItem.Index
        } | Remove-Item

        # Return the message
        $MessageQueueItem | Write-Output
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Push-Message {
<#
.SYNOPSIS
    Adds a message to one or several message queue(s).
.DESCRIPTION
    Adds a message to one or several message queue(s).
.PARAMETER MessageQueueId
    The ID of the message queue(s) where message should be added to.
.PARAMETER Label
    The label of the message.
.PARAMETER MetaData
    The metadata of the message.
.OUTPUTS
    [int] the index of the added message in the queue
.EXAMPLE
    Push-Message -Id '12345678-1234-1234-1234-123456789012' -Label 'Test' -MetaData @{ Test = 'Test' }
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')]
[OutputType([int])]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
    [guid[]]$MessageQueueId,

    [Parameter(Mandatory, Position = 1)]
    [string]$Label,

    [Parameter(Mandatory = $false, Position = 2)]
    [AllowNull()]
    [object]$MetaData = $null
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null
        
        $MessageData = [pscustomobject]@{
            MessageId = [guid]::NewGuid()
            Index = 1
            Label = $Label
            Date = ((Get-Date)|ConvertTo-JSon)
            MetaData = ($MetaData|ConvertTo-JSon)
        }

        # Add the message to the message store
        $MessageStore = New-Item -Path "MetaNull:\MessageStore\$($MessageData.MessageId)"
        $MessageStore | Set-ItemProperty -Name 'MessageId' -Value $MessageData.MessageId
        $MessageStore | Set-ItemProperty -Name 'Label' -Value $MessageData.Label
        $MessageStore | Set-ItemProperty -Name 'Date' -Value $MessageData.Date
        $MessageStore | Set-ItemProperty -Name 'MetaData' -Value $MessageData.MetaData

        # Add the reference to the message in each messagequeue
        $MessageQueueId | Foreach-Object {
            $MQID = $_
            $MessageData.Index = 1

            # Find the highest index in THIS queue
            $Item = Get-Item -Path "MetaNull:\MessageQueue\$MQID"
            $NewIndex = $Item | Get-ChildItem | Get-ItemProperty | Sort-Object -Property Index | Select-Object -Last 1 | ForEach-Object {
                $_.Index + 1
            }
            if($NewIndex -ne $null) {
                $MessageData.Index = $NewIndex
            }

            # Add the reference to THIS messagequeue
            $Message = New-Item -Path "MetaNull:\MessageQueue\$MQID\$($MessageData.Index)"
            $Message | Set-ItemProperty -Name 'MessageId' -Value $MessageData.MessageId
            $Message | Set-ItemProperty -Name 'Index' -Value $MessageData.Index
            $Message | Set-ItemProperty -Name 'Date' -Value $MessageData.Date

            # return the message id
            return $MessageData.Index
        }
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Remove-MessageQueue {
<#
.SYNOPSIS
    Remove a message queue.
.DESCRIPTION
    This function removes the whole message queue.
.PARAMETER MessageQueueId
    The ID of the message queue to be removed.
.EXAMPLE
    Remove-MessageQueue -Id '12345678-1234-1234-1234-123456789012'
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Medium')]
[OutputType([void])]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
    [guid]$MessageQueueId
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        $MetaNull.MessageQueue.MutexMessageQueue.WaitOne() | Out-Null
        # Remove the message queue
        Remove-Item "MetaNull:\MessageQueue\$MessageQueueId" -Recurse
    } finally {
        $MetaNull.MessageQueue.MutexMessageQueue.ReleaseMutex()
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}
Function Resolve-MessageQueueId {
<#
    .SYNOPSIS
        Lookup for valid Message Queue Ids, and provides Auto Completion for partial Ids
 
    .PARAMETER $PartialId
        A partial Message Queue ID
 
    .EXAMPLE
        # Direct use
        $IDs = Resolve-MessageQueueId -PartialId '123*'
 
    .EXAMPLE
        # Use as a Parameter Argument Completer
        Function MyFunction {
            param(
                [Parameter(Mandatory)]
                [SupportsWildcards()]
                [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
                [guid] $Id = [guid]::Empty,
            )
            "Autocompleted ID: $Id"
        }
#>

[CmdletBinding(DefaultParameterSetName = 'ArgumentCompleter')]
param (
    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $commandName,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $parameterName,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $wordToComplete,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $commandAst,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $fakeBoundParameters,

    [Parameter(Mandatory,ParameterSetName = 'Lookup')]
    [SupportsWildcards()]
    $PartialId
)

$PartialQueueId = '*'
if($PSCmdlet.ParameterSetName -eq 'ArgumentCompleter') {
    $PartialQueueId = "$wordToComplete*"
} elseif($PSCmdlet.ParameterSetName -eq 'Lookup') {
    $PartialQueueId = "$PartialId"
} else {
    throw "Invalid ParameterSet"
}
Get-ChildItem -Path "MetaNull:\MessageQueue" | Split-Path -Leaf | Where-Object {
    $_ -like $PartialQueueId
}
}
Function Resolve-MessageQueueName {
<#
    .SYNOPSIS
        Lookup for valid Queue Names, and provides Auto Completion for partial Names
 
    .PARAMETER $PartialName
        A partial MEssage Queue Name
 
    .EXAMPLE
        # Direct use
        $IDs = Resolve-MessageQueueName -PartialName 'Queue*'
 
    .EXAMPLE
        # Use as a Parameter Argument Completer
        Function MyFunction {
            param(
                [Parameter(Mandatory)]
                [SupportsWildcards()]
                [ArgumentCompleter( {Resolve-MessageQueueName @args} )]
                [string] $Name
            )
            "Autocompleted Name: $Name"
        }
#>

[CmdletBinding(DefaultParameterSetName = 'ArgumentCompleter')]
param ( 
    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $commandName,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $parameterName,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $wordToComplete,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $commandAst,

    [Parameter(Mandatory,ParameterSetName = 'ArgumentCompleter')]
    $fakeBoundParameters, 

    [Parameter(Mandatory,ParameterSetName = 'Lookup')]
    [SupportsWildcards()]
    $PartialName
)

$PartialQueueName = '*'
if($PSCmdlet.ParameterSetName -eq 'ArgumentCompleter') {
    $PartialQueueName = "$wordToComplete*"
} elseif($PSCmdlet.ParameterSetName -eq 'Lookup') {
    $PartialQueueName = "$PartialName"
} else {
    throw "Invalid ParameterSet"
}

Get-ChildItem -Path "MetaNull:\MessageQueue" | Get-ItemProperty | Select-Object -ExpandProperty Name | Where-Object {
    $_ -like $PartialQueueName
}
}
Function Test-MessageQueue {
<#
.SYNOPSIS
    Test if a Message Queue exists
.DESCRIPTION
    Test if a Message Queue exists
.PARAMETER MessageQueueId
    The Id of the message queue to be retrieved.
.PARAMETER Name
    Lookup by Name rather than by Id.
.EXAMPLE
    Test-MessageQueue -Id '12345678-1234-1234-1234-123456789012'
.EXAMPLE
    Test-MessageQueue -Name 'MyQueue'
.OUTPUTS
    [bool] True if the message queue exists, otherwise false.
#>

[CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')]
[OutputType([bool])]
param(
    [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
    [ArgumentCompleter( {Resolve-MessageQueueId @args} )]
    [guid]$MessageQueueId
)
Process {
    $BackupErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Stop'
    try {
        return Test-Path "MetaNull:\MessageQueue\$MessageQueueId"
    } finally {
        $ErrorActionPreference = $BackupErrorActionPreference
    }
}
}