MetaNull.Queue.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.Queue' } else { # Current user is not Administrator $PSDriveRoot = 'HKCU:\SOFTWARE\MetaNull\PowerShell\MetaNull.Queue' } if(-not (Test-Path $PSDriveRoot)) { New-Item -Path $PSDriveRoot -Force | Out-Null } New-Variable MetaNull -Scope script -Value @{ Queue = @{ PSDriveRoot = $PSDriveRoot Lock = New-Object Object Drive = New-PSDrive -Name 'MetaNull' -Scope Script -PSProvider Registry -Root $PSDriveRoot } } if(-not (Test-Path MetaNull:\Queues)) { New-Item -Path MetaNull:\Queues -Force | Out-Null } Function Test-IsAdministrator { <# .SYNOPSIS Tests if the current user has Administrative rights #> [CmdletBinding()] [OutputType([bool])] param() Process { $User = [Security.Principal.WindowsIdentity]::GetCurrent() $Principal = [Security.Principal.WindowsPrincipal]::new($User) $Role = [Security.Principal.WindowsBuiltInRole]::Administrator return $Principal.IsInRole($Role) } } Function Clear-Queue { <# .SYNOPSIS Remove all Commands from the queue .DESCRIPTION Remove all Commands from the queue .PARAMETER Id The Id of the queue .EXAMPLE Clear-Queue -Id $Id #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Medium')] [OutputType([pscustomobject])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [ArgumentCompleter( {Resolve-QueueId @args} )] [guid] $Id ) Process { $BackupErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' try { # Find the queue if(-not "$Id" -or -not (Test-Path "MetaNull:\Queues\$Id")) { throw "Queue $Id not found" } # Remove the command Remove-Item "MetaNull:\Queues\$Id\Commands\*" -Force -Recurse } finally { $ErrorActionPreference = $BackupErrorActionPreference } } } Function Get-Queue { <# .SYNOPSIS Returns the list of Queues .DESCRIPTION Returns the list of Queues .PARAMETER Id The Id of the Queue to return .PARAMETER Name The filter to apply on the name of the Queues Filter supports wildcards .EXAMPLE # Get all the Queues Get-Queue .EXAMPLE # Get the Queue with the Id '00000000-0000-0000-0000-000000000000' Get-Queue -Id '00000000-0000-0000-0000-000000000000' .EXAMPLE # Get the Queues with the name starting with 'Queue' Get-Queue -Name 'Queue*' #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'None')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [ArgumentCompleter( {Resolve-QueueId @args} )] [guid] $Id = [guid]::Empty, [Parameter(Mandatory = $false, Position = 1)] [SupportsWildcards()] [ArgumentCompleter( {Resolve-QueueName @args} )] [string] $Name = '*' ) Process { $BackupErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' try { # Set the filter to '*' or the 'Id' $Filter = '*' if($Id -ne [guid]::Empty) { $Filter = $Id.ToString() } [System.Threading.Monitor]::Enter($MetaNull.Queue.Lock) try { # Get the queue(s) Get-Item -Path "MetaNull:\Queues\$Filter" | Foreach-Object { $Queue = $_ | Get-ItemProperty | Select-Object * | Select-Object -ExcludeProperty PS* $Queue | Add-Member -MemberType NoteProperty -Name 'RegistryKey' -Value $RegistryKey $Queue | Add-Member -MemberType NoteProperty -Name 'Commands' -Value @() # Return the queue object $Queue | Write-Output } | Where-Object { # Filter the queue(s) by 'Name' $_.Name -like $Name } | ForEach-Object { # Add command(s) to the queue object $_.Commands = Get-ChildItem "MetaNull:\Queues\$($_.Id)\Commands" | Foreach-Object { $Command = $_ | Get-ItemProperty | Select-Object * | Select-Object -ExcludeProperty PS* $Command | Add-Member -MemberType NoteProperty -Name 'RegistryKey' -Value $RegistryKey $Command | Write-Output } # Return the Queue object $_ | write-output } } finally { [System.Threading.Monitor]::Exit($MetaNull.Queue.Lock) } } finally { $ErrorActionPreference = $BackupErrorActionPreference } } } Function New-Queue { <# .SYNOPSIS Create a Queue .DESCRIPTION Create a Queue .PARAMETER Name The name of the Queue .PARAMETER Description The description of the Queue .PARAMETER Status The status of the Queue (Iddle, Running, Disabled, Suspended) .EXAMPLE New-Queue -Name 'Queue1' -Description 'Queue 1' -Status 'Running' #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')] [OutputType([guid])] param( [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory=$false)] [AllowNull()] [AllowEmptyString()] [string] $Description, [Parameter(Mandatory=$false)] [ValidateSet('Iddle','Running','Disabled','Suspended')] [string] $Status = 'Iddle' ) Process { $BackupErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' try { # Create the queue object $Guid = [guid]::NewGuid().ToString() $Properties = @{ Id = $Guid Name = $Name Description = $Description Status = $Status } # Store the queue into the registry [System.Threading.Monitor]::Enter($MetaNull.Queue.Lock) try { $Item = New-Item -Path "MetaNull:\Queues\$Guid" -Force New-Item -Path "MetaNull:\Queues\$Guid\Commands" -Force | Out-Null $Properties.GetEnumerator() | ForEach-Object { $Item | New-ItemProperty -Name $_.Key -Value $_.Value | Out-Null } return $Guid } finally { [System.Threading.Monitor]::Exit($MetaNull.Queue.Lock) } } finally { $ErrorActionPreference = $BackupErrorActionPreference } } } Function Pop-QueueCommand { <# .SYNOPSIS Remove a Command from the top or bottom of the queue .DESCRIPTION Remove a Command from the top or bottom of the queue .PARAMETER Id The Id of the queue .PARAMETER Unshift If set, removes the command from the top of the queue .OUTPUTS [pscustomobject] .EXAMPLE $Command = Pop-QueueCommand -Id $Id $ScriptBlock = $Command.ToScriptBlock() $ScriptBlock.Invoke() .EXAMPLE Pop-QueueCommand -Id $Id -Unshift $ScriptBlock = $Command.ToScriptBlock() $ScriptBlock.Invoke() #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low')] [OutputType([pscustomobject])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [ArgumentCompleter( {Resolve-QueueId @args} )] [guid] $Id, [Parameter(Mandatory = $false)] [switch] $Unshift ) Process { $BackupErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' try { # Collect the existing commands $Commands = Get-ChildItem "MetaNull:\Queues\$Id\Commands" | Foreach-Object { $_ | Get-ItemProperty | Select-Object * | Select-Object -ExcludeProperty PS* | Write-Output } | Sort-Object -Property Index # Select which command to remove if($Unshift.IsPresent -and $Unshift) { $Command = $Commands | Select-Object -First 1 } else { $Command = $Commands | Select-Object -Last 1 } if(-not $Command -or -not $Command.Index) { return } # Remove the command from the registry Remove-Item -Force -Recurse "MetaNull:\Queues\$Id\Commands\$($Command.Index)" # Return the popped/unshifted command $Command | Add-Member -MemberType ScriptMethod -Name ToScriptBlock -Value { try { return [System.Management.Automation.ScriptBlock]::Create($this.Command -join "`n") } catch { return $null } } $Command | Write-Output } finally { $ErrorActionPreference = $BackupErrorActionPreference } } } Function Push-QueueCommand { <# .SYNOPSIS Add a new Command at the end of a queue .DESCRIPTION Add a new Command at the end of a queue .PARAMETER Id The Id of the queue .PARAMETER Commands An array of commands to add to the queue .PARAMETER Command A command to add to the queue .PARAMETER ExpandableCommand An expandable command to add to the queue (environment variables are expanded at runtime) .PARAMETER Name The name of the command .PARAMETER Unique If set, the command is only added if it is not already present in the queue .EXAMPLE Push-QueueCommand -Id $Id -Commands 'Get-Process', 'Get-Service' #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'Low',DefaultParameterSetName='REG_MULTI_SZ')] [OutputType([int])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [ArgumentCompleter( {Resolve-QueueId @args} )] [guid] $Id, [Parameter(Mandatory, Position = 1, ParameterSetName = 'REG_MULTI_SZ')] [string[]] $Commands, [Parameter(Mandatory, Position = 1, ParameterSetName = 'REG_SZ')] [string[]] $Command, [Parameter(Mandatory, Position = 1, ParameterSetName = 'REG_EXPAND_SZ')] [string[]] $ExpandableCommand, [Parameter(Mandatory = $false, Position = 2)] [AllowEmptyString()] [AllowNull()] [string] $Name, [Parameter(Mandatory = $false)] [switch] $Unique ) Process { $BackupErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' try { # Collect the existing commands $CommandList = [object[]](Get-ChildItem "MetaNull:\Queues\$Id\Commands" -ErrorAction SilentlyContinue | Foreach-Object { $_ | Get-ItemProperty | Select-Object * | Select-Object -ExcludeProperty PS* | Write-Output } | Sort-Object -Property Index) # Check if the command is already present if($Unique.IsPresent -and $Unique -and ($Commands | Where-Object { ($_.Command -join "`n") -eq ($Command -join "`n") })) { throw "Command already present in queue $Id" } # Find the last command index $LastCommandIndex = ($CommandList | Select-Object -Last 1 | Select-Object -ExpandProperty Index) + 1 # Create the new command $Properties = @{ Index = $LastCommandIndex Name = $Name # Command = $Command # This value is set separately, as it allows different types (REG_SZ, REG_MULTI_SZ, REG_EXPAND_SZ) } # Add the new command to the registry $Item = New-Item "MetaNull:\Queues\$Id\Commands\$LastCommandIndex" -Force switch($PSCmdlet.ParameterSetName) { 'REG_EXPAND_SZ' { $Item | New-ItemProperty -Name Command -Value $ExpandableCommand -Type ExpandString | Out-Null } 'REG_SZ' { $Item | New-ItemProperty -Name Command -Value $Command -Type String | Out-Null } default { $Item | New-ItemProperty -Name Command -Value ([string[]]$Commands) -Type MultiString | Out-Null } } $Properties.GetEnumerator() | ForEach-Object { $Item | New-ItemProperty -Name $_.Key -Value $_.Value | Out-Null } # Return the command Index return $LastCommandIndex } finally { $ErrorActionPreference = $BackupErrorActionPreference } } } Function Remove-Queue { <# .SYNOPSIS Removes a Queue .DESCRIPTION Removes a Queue, and all its commands .PARAMETER Id The Id of the Queue to remove .PARAMETER Force Force the removal of the Queue #> [CmdletBinding(SupportsShouldProcess,ConfirmImpact = 'High')] [OutputType([void])] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [ArgumentCompleter( {Resolve-QueueId @args} )] [guid] $Id, [Parameter(Mandatory = $false)] [switch] $Force ) Process { $BackupErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' try { # Test if the queue exists if(-not "$Id" -or -not (Test-Path "MetaNull:\Queues\$Id")) { throw "Queue $Id not found" } # Remove the queue $DoForce = $Force.IsPresent -and $Force Remove-Item "MetaNull:\Queues\$Id" -Recurse -Force:$DoForce } finally { $ErrorActionPreference = $BackupErrorActionPreference } } } Function Resolve-QueueId { <# .SYNOPSIS Lookup for valid Queue Ids, and provides Auto Completion for partial Ids .PARAMETER $PartialId A partial Queue ID .EXAMPLE # Direct use $IDs = Resolve-QueueId -PartialId '123*' .EXAMPLE # Use as a Parameter Argument Completer Function MyFunction { param( [Parameter(Mandatory)] [SupportsWildcards()] [ArgumentCompleter( {Resolve-QueueId @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:\Queues" | Split-Path -Leaf | Where-Object { $_ -like $PartialQueueId } } Function Resolve-QueueName { <# .SYNOPSIS Lookup for valid Queue Names, and provides Auto Completion for partial Names .PARAMETER $PartialName A partial Queue Name .EXAMPLE # Direct use $IDs = Resolve-QueueName -PartialName 'Queue*' .EXAMPLE # Use as a Parameter Argument Completer Function MyFunction { param( [Parameter(Mandatory)] [SupportsWildcards()] [ArgumentCompleter( {Resolve-QueueName @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:\Queues" | Get-ItemProperty | Select-Object -ExpandProperty Name | Where-Object { $_ -like $PartialQueueName } } |