├── @Delay.ps1
├── @FileChange.ps1
├── @Repeat.ps1
├── @Time.ps1
├── Clear-EventSource.ps1
├── EventSources
├── @HttpResponse.ps1
├── @Job.ps1
├── @ModuleChanged.ps1
├── @PowerShellAsync.ps1
├── @Process.ps1
├── @UDP.ps1
└── @VariableSet.ps1
├── Formatting
└── OnQ.EventSource.format.ps1
├── Get-EventSource.ps1
├── LICENSE
├── OnQ.azure-pipeline.yml
├── OnQ.ezout.ps1
├── OnQ.format.ps1xml
├── OnQ.psd1
├── OnQ.psm1
├── OnQ.tests.ps1
├── OnQ.types.ps1xml
├── README.md
├── Receive-Event.ps1
├── Send-Event.ps1
├── Types
└── OnQ.EventSource
│ ├── get_Description.ps1
│ ├── get_EventSourceID.ps1
│ ├── get_Help.ps1
│ └── get_Synopsis.ps1
├── Understanding_Event_Sources.md
├── Watch-Event.ps1
└── en-us
├── Understanding_Event_Sources.help.txt
└── about_OnQ.help.txt
/@Delay.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Send an event after a delay.
4 | .Description
5 | Send an event after waiting an arbitrary [Timespan]
6 | .Example
7 | On Delay "00:00:01" -Then { "In a second!" | Out-Host }
8 | #>
9 |
10 | param(
11 | # The amount of time to wait
12 | [Parameter(Mandatory,Position=0,ParameterSetName='Delay',ValueFromPipelineByPropertyName=$true)]
13 | [Alias('Delay', 'In')]
14 | [Timespan]
15 | $Wait
16 | )
17 |
18 | process {
19 | $timer = New-Object Timers.Timer -Property @{Interval=$Wait.TotalMilliseconds;AutoReset=$false}
20 | $timer.Start()
21 | $timer | Add-Member NoteProperty EventName Elapsed -PassThru
22 | }
--------------------------------------------------------------------------------
/@FileChange.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Watches for File Changes.
4 | .Description
5 | Uses the [IO.FileSystemWatcher] to watch for changes to files.
6 |
7 | Because some applications and frameworks write to files differently,
8 | you may see more than one event for a given change.
9 | #>
10 | param(
11 | # The path to the file or directory
12 | [Parameter(ValueFromPipelineByPropertyName)]
13 | [Alias('Fullname')]
14 | [string]
15 | $FilePath = "$pwd",
16 |
17 | # A wildcard filter describing the names of files to watch
18 | [Parameter(ValueFromPipelineByPropertyName)]
19 | [string]
20 | $FileFilter,
21 |
22 | # A notify filter describing the file changes that should raise events.
23 | [Parameter(ValueFromPipelineByPropertyName)]
24 | [IO.NotifyFilters[]]
25 | $NotifyFilter = @("FileName", "DirectoryName", "LastWrite"),
26 |
27 | # If set, will include subdirectories in the watcher.
28 | [Alias('InludeSubsdirectory','InludeSubsdirectories')]
29 | [switch]
30 | $Recurse,
31 |
32 | # The names of the file change events to watch.
33 | # By default, watches for Changed, Created, Deleted, or Renamed
34 | [ValidateSet('Changed','Created','Deleted','Renamed')]
35 | [string[]]
36 | $EventName = @('Changed','Created','Deleted','Renamed')
37 | )
38 |
39 | process {
40 | $resolvedFilePath = try {
41 | $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($FilePath)
42 | } catch {
43 | Write-Error "Could not resolve path '$FilePath'"
44 | return
45 | }
46 |
47 | if ([IO.File]::Exists("$resolvedFilePath")) { # If we're passed a path to a specific file
48 | $fileInfo = ([IO.FileInfo]"$resolvedFilePath")
49 | $filePath = $fileInfo.Directory.FullName # we need to watch the directory
50 | $FileFilter = $fileInfo.Name # and then filter based off of the file name.
51 | } elseif ([IO.Directory]::Exists("$resolvedFilePath")) {
52 | $filePath = "$resolvedFilePath"
53 | }
54 |
55 | $fileSystemWatcher = [IO.FileSystemWatcher]::new($FilePath) # Create the watcher
56 | $fileSystemWatcher.IncludeSubdirectories = $Recurse # include subdirectories if -Recurse was passed
57 | $fileSystemWatcher.EnableRaisingEvents = $true # Enable raising events
58 | if ($FileFilter) {
59 | $fileSystemWatcher.Filter = $FileFilter
60 | } else {
61 | $fileSystemWatcher.Filter = "*"
62 | }
63 | $combinedNotifyFilter = 0
64 | foreach ($n in $NotifyFilter) {
65 | $combinedNotifyFilter = $combinedNotifyFilter -bor $n
66 | }
67 | $fileSystemWatcher.NotifyFilter = $combinedNotifyFilter
68 | $fileSystemWatcher |
69 | Add-Member NoteProperty EventName $EventName -Force -PassThru
70 | }
71 |
--------------------------------------------------------------------------------
/@Repeat.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Send events on repeat.
4 | .Description
5 | Sends events on repeat, at a given [Timespan] -Interval.
6 | .Example
7 | On Interval "00:01:00" { "Every minute" | Out-Host }
8 | #>
9 | [Diagnostics.Tracing.EventSource(Name='Elapsed')]
10 | param(
11 | # The amount of time to wait between sending events.
12 | [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName=$true)]
13 | [Alias('Every')]
14 | [Timespan]
15 | $Interval
16 | )
17 |
18 | process {
19 | $timer = New-Object Timers.Timer -Property @{Interval=$Interval.TotalMilliseconds;AutoReset=$true}
20 | $timer.Start()
21 | $timer
22 | }
--------------------------------------------------------------------------------
/@Time.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Sends an event at a specific time.
4 | .Description
5 | Sends an event at a specific date and time.
6 | .Example
7 | On Time "5:00 PM" { "EOD!" | Out-Host }
8 | #>
9 | [Diagnostics.Tracing.EventSource(Name='Elapsed')]
10 | param(
11 | [Parameter(Mandatory,Position=0,ParameterSetName='SpecificTime')]
12 | [DateTime]
13 | $DateTime
14 | )
15 |
16 | process {
17 | if ($DateTime -lt [DateTime]::Now) {
18 | Write-Error "-DateTime '$DateTime' must be in the future"
19 | return
20 | }
21 |
22 | $timer =
23 | New-Object Timers.Timer -Property @{Interval=($DateTime - [DateTime]::Now).TotalMilliseconds;AutoReset=$false}
24 |
25 | if (-not $timer) { return }
26 | $timer.Start()
27 | return $timer
28 | }
29 |
--------------------------------------------------------------------------------
/Clear-EventSource.ps1:
--------------------------------------------------------------------------------
1 | function Clear-EventSource
2 | {
3 | <#
4 | .Synopsis
5 | Clears event source subscriptions
6 | .Description
7 | Clears any active subscriptions for any event source.
8 | .Example
9 | Clear-EventSource
10 | .Link
11 | Get-EventSource
12 | #>
13 | [CmdletBinding(SupportsShouldProcess=$true)]
14 | [OutputType([nullable])]
15 | param(
16 | # The name of the event source.
17 | [Parameter(ValueFromPipelineByPropertyName)]
18 | [string[]]
19 | $Name)
20 |
21 | process {
22 | #region Determine Event Sources
23 | $parameterCopy = @{} + $PSBoundParameters
24 | $null = $parameterCopy.Remove('WhatIf')
25 | $eventSources = Get-EventSource @parameterCopy -Subscription
26 | if ($WhatIfPreference) {
27 | $eventSources
28 | return
29 | }
30 | #endregion Determine Event Sources
31 | #region Unregister
32 | if ($PSCmdlet.ShouldProcess("Clear event sources $($Name -join ',')")) {
33 | $eventSources | Unregister-Event
34 | }
35 | #endregion Unregister
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/EventSources/@HttpResponse.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Sends events on HTTP Responses.
4 | .Description
5 | Sends HTTP requests and signals on Responses
6 |
7 |
8 | Event MessageData will contain the response, with two additional properties:
9 | * .ResponseBytes
10 | * .ResponseContent
11 | #>
12 | [ComponentModel.InitializationEvent({
13 | # Because of the potential for a "narrow window" timing issue,
14 | # this event source needs to send the request the moment after the event has been subscribed to.
15 | $httpAsyncInfo = [PSCustomObject]@{
16 | InputObject = $this
17 | IAsyncResult = $this.BeginGetResponse($null, $null)
18 | InvokeID = $this.RequestID
19 | }
20 | if ($null -eq $Global:HttpResponsesAsync) {
21 | $Global:HttpResponsesAsync = [Collections.ArrayList]::new()
22 | }
23 | $null = $Global:HttpResponsesAsync.Add($httpAsyncInfo)
24 | })]
25 | param(
26 | # The Uniform Resource Identifier.
27 | [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName)]
28 | [Alias('Url')]
29 | [Uri]
30 | $Uri,
31 |
32 | # The HTTP Method
33 | [Parameter(Position=1,ValueFromPipelineByPropertyName)]
34 | [ValidateSet('Get','Head','Post','Put','Delete','Trace','Options','Merge','Patch')]
35 | [string]
36 | $Method = 'GET',
37 |
38 | # A collection of headers to send with the request.
39 | [Parameter(Position=2,ValueFromPipelineByPropertyName)]
40 | [Alias('Headers')]
41 | [Collections.IDictionary]
42 | $Header,
43 |
44 | # The request body.
45 | [Parameter(Position=3,ValueFromPipelineByPropertyName)]
46 | [PSObject]
47 | $Body,
48 |
49 | # The polling interval.
50 | # This is the minimum amount of time until you will be notified of the success or failure of a http request
51 | [Parameter(Position=3,ValueFromPipelineByPropertyName)]
52 | [Timespan]
53 | $PollingInterval = "00:00:00.331",
54 |
55 | [Text.Encoding]
56 | $TransferEncoding = $([Text.Encoding]::UTF8)
57 | )
58 |
59 | process {
60 | # Clear the event subscriber if one exists.
61 | $eventSubscriber = Get-EventSubscriber -SourceIdentifier "@HttpResponse_Check" -ErrorAction SilentlyContinue
62 | if ($eventSubscriber) {$eventSubscriber | Unregister-Event}
63 |
64 | # Create a new subscriber for the request.
65 | $httpResponseCheckTimer = New-Object Timers.Timer -Property @{
66 | Interval = $PollingInterval.TotalMilliseconds # Every pollinginterval,
67 | AutoReset = $true
68 | }
69 | $HttpResponseChecker =
70 | Register-ObjectEvent -InputObject $httpResponseCheckTimer -EventName Elapsed -Action {
71 | $toCallEnd = # check to see if any requests have completed.
72 | @(foreach ($httpAsyncInfo in $Global:HttpResponsesAsync) {
73 | if ($httpAsyncInfo.IAsyncResult.IsCompleted) {
74 | $httpAsyncInfo
75 | }
76 | })
77 |
78 | $null = # Foreach completed request
79 | foreach ($httpAsyncInfo in $toCallEnd) {
80 | $webResponse =
81 | try { # try to get the response
82 | $httpAsyncInfo.InputObject.EndGetResponse($httpAsyncInfo.IAsyncResult)
83 | } catch {
84 | $_ # and catch any error.
85 | }
86 |
87 |
88 | if ($webResponse -is [Management.Automation.ErrorRecord] -or
89 | $webResponse -is [Exception]) # If we got an error
90 | {
91 | # Signal the error
92 | New-Event -SourceIdentifier "HttpRequest.Failed.$($httpAsyncInfo.InvokeID)" -MessageData $webResponse
93 | $Global:HttpResponsesAsync.Remove($httpAsyncInfo)
94 | continue
95 | }
96 |
97 |
98 | # Otherwise, get the response stream
99 | $ms = [IO.MemoryStream]::new()
100 | $null = $webResponse.GetResponseStream().CopyTo($ms)
101 | $responseBytes = $ms.ToArray() # as a [byte[]].
102 | $ms.Close()
103 | $ms.Dispose()
104 |
105 |
106 | $encoding = # See if the response had an encoding
107 | if ($webResponse.ContentEncoding) { $webResponse.ContentEncoding }
108 | elseif ($webResponse.CharacterSet) {
109 | # or a character set.
110 | [Text.Encoding]::GetEncodings() | Where-Object Name -EQ $webResponse.CharacterSet
111 | }
112 |
113 | $webResponseContent =
114 | if ($encoding) { # If it did, decode the response content.
115 | [IO.StreamReader]::new([IO.MemoryStream]::new($responseBytes), $encoding).ReadToEnd()
116 | } else {
117 | $null
118 | }
119 |
120 | # Add the properties to the web response.
121 | $webResponse |
122 | Add-Member NoteProperty ResponseBytes $webResponseContent -Force -PassThru |
123 | Add-Member NoteProperty ResponseContent $webResponseContent -Force
124 | $webResponse.Close()
125 |
126 | # And send the response with the additional information.
127 | New-Event -SourceIdentifier "HttpRequest.Completed.$($httpAsyncInfo.InvokeID)" -MessageData $webResponse
128 | $Global:HttpResponsesAsync.Remove($httpAsyncInfo)
129 | }
130 |
131 | if ($Global:HttpResponsesAsync.Count -eq 0) {
132 | Get-EventSubscriber -SourceIdentifier "@HttpResponse_Check" | Unregister-Event
133 | }
134 | }
135 |
136 | $httpResponseCheckTimer.Start() # Start it's timer.
137 |
138 |
139 | $httpRequest = [Net.HttpWebRequest]::CreateHttp($Uri)
140 | $httpRequest.Method = $Method
141 | if ($Header -and $Header.Count) {
142 | foreach ($kv in $Header.GetEnumerator()) {
143 | $httpRequest.Headers[$kv.Key] = $kv.Value
144 | }
145 | }
146 | if ($Body) {
147 | $requestStream = $httpRequest.GetRequestStream()
148 |
149 | if (-not $requestStream) { return }
150 | if ($body -is [byte[]] -or $Body -as [byte[]]) {
151 | [IO.MemoryStream]::new([byte[]]$Body).CopyTo($requestStream)
152 | }
153 | elseif ($Body -is [string]) {
154 | [IO.StreamWriter]::new($requestStream, $TransferEncoding).Write($Body)
155 | }
156 | else {
157 | [IO.StreamWriter]::new($requestStream, $TransferEncoding).Write((ConvertTo-Json -InputObject $body -Depth 100))
158 | }
159 | }
160 | $requestId = [Guid]::NewGuid().ToString()
161 | $httpRequest |
162 | Add-Member NoteProperty SourceIdentifier "HttpRequest.Completed.$requestId","HttpRequest.Failed.$requestId" -Force -PassThru |
163 | Add-Member NoteProperty RequestID $requestId -Force -PassThru
164 | }
--------------------------------------------------------------------------------
/EventSources/@Job.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Watches a PowerShell Job's State.
4 | .Description
5 | Watches a PowerShell Job's StateChange event.
6 |
7 | This will send an event when a job finishes.
8 | #>
9 | param(
10 | [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
11 | [Alias('ID')]
12 | [int]
13 | $JobID)
14 |
15 | if ($_ -is [Management.Automation.Job]) {
16 | $_
17 | } else {
18 | Get-Job -Id $JobID -ErrorAction SilentlyContinue
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/EventSources/@ModuleChanged.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Watches for Module loads and unloads.
4 | .Description
5 | Polls the current set of globally imported PowerShell modules.
6 |
7 | When this changes, one of two events will be generated:
8 |
9 | PowerShell.Module.Loaded will be sent when one or more modules is loaded
10 | PowerShell.Module.Unloaded will be sent when one or more modules is unloaded
11 |
12 | Only one event if each will be sent per polling interval.
13 | #>
14 | param(
15 | # The frequency to check for a module load.
16 | [Timespan]
17 | $PollingInterval = [Timespan]::FromMilliseconds(7037)
18 | )
19 |
20 | process {
21 | $eventSubscriber = Get-EventSubscriber -SourceIdentifier "ModuleLoaded_Check" -ErrorAction SilentlyContinue
22 | if ($eventSubscriber) {
23 | $eventSubscriber | Unregister-Event
24 | }
25 |
26 | $timer = [Timers.Timer]::new()
27 | $timer.AutoReset = $true
28 | $timer.Interval = $PollingInterval.TotalMilliseconds
29 | $timer.Start()
30 | Register-ObjectEvent -SourceIdentifier "ModuleLoaded_Check" -InputObject $timer -EventName Elapsed -Action {
31 | $loadedModules = Get-Module
32 | if ($script:ModuleLoadedList) {
33 | $Compared = Compare-Object -ReferenceObject $script:ModuleLoadedList -DifferenceObject $loadedModules
34 |
35 | $newModules = $compared | Where-Object SideIndicator -eq '=>' | Select-Object -ExpandProperty InputObject
36 | $removedModules = $compared | Where-Object SideIndicator -eq '<=' | Select-Object -ExpandProperty InputObject
37 | if ($newModules) {
38 | New-Event -SourceIdentifier "PowerShell.Module.Loaded" -EventArguments $newModules -MessageData $newModules
39 | }
40 | if ($removedModules) {
41 | New-Event -SourceIdentifier "PowerShell.Module.Unloaded" -EventArguments $removedModules -MessageData $removedModules
42 | }
43 | }
44 | $script:ModuleLoadedList = $loadedModules
45 | } | Out-Null
46 | [PSCustomObject]@{
47 | SourceIdentifier = "PowerShell.Module.Loaded","PowerShell.Module.Unloaded"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/EventSources/@PowerShellAsync.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Runs PowerShell asynchronously
4 | .Description
5 | Runs PowerShell in the background.
6 | Events are fired on the completion or failure of the PowerShell command.
7 | #>
8 | [ComponentModel.InitializationEvent({
9 | # Because of the potential for a "narrow window" timing issue,
10 | # this event source needs to return a script to run in order to start it.
11 | # $this is whatever object is returned.
12 | # If multiple objects are returned, this will be run multiple times.
13 | $Global:PowerAsyncResults[$this.InstanceID] = $this.BeginInvoke()
14 | })]
15 | [CmdletBinding(PositionalBinding=$false)]
16 | param(
17 | # The scripts you would like to run. Each script block will be counted as a distinct statement.
18 | [Parameter(Mandatory,Position=0)]
19 | [ScriptBlock[]]
20 | $ScriptBlock,
21 |
22 | # The named parameters passed to each script.
23 | [Collections.IDictionary[]]
24 | [Alias('Parameters')]
25 | $Parameter,
26 |
27 | # If provided, will run in a specified runspace. The Runspace must already be open.
28 | [Management.Automation.Runspaces.Runspace]
29 | $Runspace,
30 |
31 | # If provided, will run in a runspace pool. The RunspacePool must already be open.
32 | [Management.Automation.Runspaces.RunspacePool]
33 | $RunspacePool
34 | )
35 |
36 | process {
37 | $psAsync = [PowerShell]::Create()
38 | $null = for ($n =0; $n -lt $ScriptBlock.Length;$n++) {
39 | $psAsync.AddStatement()
40 | $psAsync.AddScript($ScriptBlock[$n])
41 | if ($parameter -and $Parameter[$n]) {
42 | $psAsync.AddParameters($Parameter[$n])
43 | }
44 | }
45 | foreach ($cmd in $psAsync.Commands) {
46 | foreach ($ce in $cmd.Commands) {
47 | $ce.MergeMyResults("All", "Output")
48 | }
49 | }
50 | if (-not $Global:PowerAsyncResults) {
51 | $Global:PowerAsyncResults = @{}
52 | }
53 |
54 | if ($Runspace) {
55 | $psAsync.Runspace = $Runspace
56 | }
57 | elseif ($RunspacePool) {
58 | $psAsync.RunspacePool = $RunspacePool
59 | }
60 |
61 | $onFinishedSubscriber =
62 | Register-ObjectEvent -InputObject $psAsync -EventName InvocationStateChanged -Action {
63 | $psAsyncCmd = $event.Sender
64 | if ($psAsyncCmd.InvocationStateInfo.State -notin 'Completed', 'Failed', 'Stopped') {
65 | return
66 | }
67 | if ($Global:PowerAsyncResults[$psAsyncCmd.InstanceID]) {
68 | $asyncResults = $event.Sender.EndInvoke($Global:PowerAsyncResults[$psAsyncCmd.InstanceID])
69 | $Global:PowerAsyncResults.Remove($psAsyncCmd.InstanceID)
70 | New-Event -SourceIdentifier "PowerShell.Async.$($psAsyncCmd.InstanceID)" -Sender $event.Sender -MessageData $asyncResults -EventArguments $event.SourceEventArgs
71 | }
72 |
73 | if ($psAsyncCmd.InvocationStateInfo.Reason) {
74 | New-Event -SourceIdentifier "PowerShell.Async.Failed.$($psAsyncCmd.InstanceID)" -Sender $psAsyncCmd -MessageData $psAsyncCmd.InvocationStateInfo.Reason
75 | }
76 | Get-EventSubscriber |
77 | Where-Object SourceObject -EQ $psAsyncCmd |
78 | Unregister-Event
79 |
80 | $psAsyncCmd.Dispose()
81 | }
82 |
83 | $psAsync |
84 | Add-Member NoteProperty SourceIdentifier @(
85 | "PowerShell.Async.$($psAsync.InstanceID)","PowerShell.Async.Failed.$($psAsync.InstanceID)"
86 | ) -Force -PassThru
87 |
88 | return
89 | }
--------------------------------------------------------------------------------
/EventSources/@Process.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Watches a process.
4 | .Description
5 | Watches a process.
6 |
7 | If -Exit is passed, watches for process exit.
8 |
9 | If -Output is passed, watches for process output
10 |
11 | If -Error is passed, watched for process error
12 | #>
13 | param(
14 | # The process identifier
15 | [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
16 | [Alias('ID')]
17 | [int]
18 | $ProcessID,
19 |
20 | # If set, will watch for process exit. This is the default unless -StandardError or -StandardOutput are passed.
21 | [switch]
22 | $Exit,
23 |
24 | # If set, will watch for new standard output.
25 | [switch]
26 | $StandardOutput,
27 |
28 | # If set, will watch for new standard erorr.
29 | [switch]
30 | $StandardError
31 | )
32 |
33 | process {
34 | $eventNames = @(
35 | if ($Exit) {
36 | "Exited"
37 | }
38 | if ($StandardOutput) {
39 | "OutputDataReceived"
40 | }
41 | if ($StandardError) {
42 | "ErrorDataReceived"
43 | }
44 | )
45 |
46 | if ($eventNames) {
47 | Get-Process -Id $ProcessID |
48 | Add-Member EventName $eventNames -Force -PassThru
49 | } else {
50 | Get-Process -Id $ProcessID |
51 | Add-Member EventName "Exited" -Force -PassThru
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/EventSources/@UDP.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Signals on UDP
4 | .Description
5 | Runs PowerShell in the background.
6 | Events are fired on the completion or failure of the PowerShell command.
7 | #>
8 | param(
9 | # The IP Address where UDP packets can originate. By default, [IPAddress]::Any.
10 | [Parameter(ValueFromPipelineByPropertyName)]
11 | [Net.IPAddress]
12 | $IPAddress = [Net.IPAddress]::Any,
13 |
14 | # The Port used to listen for packets.
15 | [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
16 | [int]
17 | $Port,
18 |
19 | # The encoding. If provided, packet content will be decoded.
20 | [Parameter(ValueFromPipelineByPropertyName)]
21 | [Text.Encoding]
22 | $Encoding
23 | )
24 |
25 | $startedJob = Start-Job -ScriptBlock {
26 | param($IPAddress, $port, $encoding)
27 |
28 | $udpSvr= [Net.Sockets.UdpClient]::new()
29 | $endpoint = [Net.IpEndpoint]::new($IPAddress, $Port)
30 |
31 | try {
32 | $udpSvr.Client.Bind($endpoint)
33 | } catch {
34 | Write-Error -Message $_.Message -Exception $_
35 | return
36 | }
37 | $eventSourceId = "UDP.${IPAddress}:$port"
38 | Register-EngineEvent -SourceIdentifier $eventSourceId -Forward
39 | if ($encoding) {
40 | $RealEncoding = [Text.Encoding]::GetEncoding($encoding.BodyName)
41 | if (-not $RealEncoding) {
42 | throw "Could not find $($encoding | Out-String)"
43 | }
44 | }
45 | while ($true) {
46 | $packet = $udpSvr.Receive([ref]$endpoint)
47 |
48 | if ($RealEncoding) {
49 | New-Event -Sender $IPAddress -MessageData $RealEncoding.GetString($packet) -SourceIdentifier $eventSourceId |
50 | Out-Null
51 | } else {
52 | New-Event -Sender $IPAddress -MessageData $packet -SourceIdentifier $eventSourceId | Out-Null
53 | }
54 |
55 | }
56 | } -ArgumentList $IPAddress, $port, $Encoding
57 |
58 | @{
59 | SourceIdentifier = "UDP.${IPAddress}:$port"
60 | }
61 |
62 |
63 |
--------------------------------------------------------------------------------
/EventSources/@VariableSet.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .Synopsis
3 | Watches for variable sets.
4 | .Description
5 | Watches for assignments to a variable.
6 |
7 | Events are sent directly after the variable is set.
8 |
9 | The -Sender is the callstack, The -MessageData is the value of the variable.
10 | #>
11 | param(
12 | # The name of the variable
13 | [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
14 | [string]
15 | $VariableName
16 | )
17 |
18 | process {
19 | Get-PSBreakpoint | Where-Object Variable -eq $VariableName | Remove-PSBreakpoint
20 |
21 | $raiseEvent = [ScriptBlock]::Create(@"
22 | New-Event -SourceIdentifier 'VariableSet.$variableName' -MessageData `$$variableName -Sender (Get-PSCallstack)
23 | continue
24 | "@)
25 | Set-PSBreakpoint -Variable $VariableName -Action $raiseEvent | Out-Null
26 |
27 | [PSCustomObject]@{SourceIdentifier="VariableSet.$variableName"}
28 | }
29 |
--------------------------------------------------------------------------------
/Formatting/OnQ.EventSource.format.ps1:
--------------------------------------------------------------------------------
1 | Write-FormatView -TypeName OnQ.EventSource -Property Name, Synopsis, Parameters -VirtualProperty @{
2 | Parameters = {
3 | @(foreach ($kv in ([Management.Automation.CommandMetaData]$_).Parameters.GetEnumerator()) {
4 | @(
5 | . $setOutputStyle -ForegroundColor Verbose
6 | "[$($kv.Value.ParameterType)]"
7 | . $clearOutputStyle
8 | . $setOutputStyle -ForegroundColor Warning
9 | "`$$($kv.Key)"
10 | . $clearOutputStyle
11 | ) -join ''
12 | }) -join [Environment]::NewLine
13 | }
14 | } -Wrap -ColorProperty @{
15 | "Name" = {"Success"}
16 | }
17 |
--------------------------------------------------------------------------------
/Get-EventSource.ps1:
--------------------------------------------------------------------------------
1 | function Get-EventSource
2 | {
3 | <#
4 | .Synopsis
5 | Gets Event Sources
6 | .Description
7 | Gets Event Sources.
8 |
9 | Event sources are commands or script blocks that can generate events.
10 |
11 | Event sources can be implemented in:
12 | * A .PS1 file starting with @
13 | * An in-memory scriptblock variable starting with @
14 | * A module command referenced within a PrivateData.OnQ section of the module manifest.
15 | .Example
16 | Get-EventSource
17 | .Example
18 | Get-EventSource -Subscription
19 | .Link
20 | Watch-Event
21 | #>
22 | [OutputType('OnQ.EventSource',
23 | [Management.Automation.CommandInfo],
24 | [Management.Automation.PSEventSubscriber],
25 | [PSObject])]
26 | param(
27 | # The name of the event source.
28 | [Parameter(ValueFromPipelineByPropertyName)]
29 | [string[]]
30 | $Name,
31 |
32 | # If set, will get subscriptions related to event sources.
33 | [Parameter(ValueFromPipelineByPropertyName)]
34 | [switch]
35 | $Subscription,
36 |
37 | # If set, will get source objects from the subscriptions related to event sources.
38 | [Parameter(ValueFromPipelineByPropertyName)]
39 | [switch]
40 | $SourceObject,
41 |
42 | # If set, will get full help for each event source.
43 | [Parameter(ValueFromPipelineByPropertyName)]
44 | [switch]
45 | $Help
46 | )
47 | begin {
48 | #region Discover Event Sources
49 | $atFunctions = $ExecutionContext.SessionState.InvokeCommand.GetCommands('@*', 'Function',$true)|
50 | Where-Object { $_.Value -is [ScriptBlock] }
51 |
52 | # Save a pointer to the method for terseness and speed.
53 | $getCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand
54 |
55 | $myInv = $MyInvocation
56 |
57 | $lookInDirectory = @(
58 | "$pwd"
59 | $myRoot = $myInv.MyCommand.ScriptBlock.File | Split-Path -ErrorAction SilentlyContinue
60 | "$myRoot"
61 | if ($myInv.MyCommand.Module) { # Assuming, of course, we have a module.
62 | $MyModuleRoot = $myInv.MyCommand.Module | Split-Path -ErrorAction SilentlyContinue
63 | if ($MyModuleRoot -ne $myRoot) { "$MyModuleRoot" }
64 | }
65 | ) | Select-Object -Unique
66 |
67 | $atScripts = $lookInDirectory |
68 | Get-Item |
69 | Get-ChildItem -Filter '@*.ps1' |
70 | & { process {
71 | $getCmd.Invoke($_.Fullname,'ExternalScript')
72 | } }
73 |
74 |
75 |
76 | # If we had a module, and we still don't have a match, we'll look for extensions.
77 |
78 | $loadedModules = @(Get-Module)
79 |
80 | if ($loadedModules -notcontains $myInv.MyCommand.Module) {
81 | $loadedModules = @($myInv.MyCommand.Module) + $loadedModules
82 | }
83 | $extendedCommands =
84 |
85 | foreach ($loadedModule in $loadedModules) { # Walk over all modules.
86 | if ( # If the module has PrivateData keyed to this module
87 | $loadedModule.PrivateData.($myInv.MyCommand.Module.Name)
88 | ) {
89 | $thisModuleRoot = [IO.Path]::GetDirectoryName($loadedModule.Path)
90 | $extensionData = $loadedModule.PrivateData.($myInv.MyCommand.Module.Name)
91 | if ($extensionData -is [Hashtable]) {
92 | foreach ($ed in $extensionData.GetEnumerator()) {
93 |
94 | $extensionCmd =
95 | if ($ed.Value -like '*.ps1') {
96 | $getCmd.Invoke(
97 | [IO.Path]::Combine($thisModuleRoot, $ed.Value),
98 | 'ExternalScript'
99 | )
100 | } else {
101 | $loadedModule.ExportedCommands[$ed.Value]
102 | }
103 | if ($extensionCmd) {
104 | $extensionCmd
105 | }
106 | }
107 | }
108 | }
109 | }
110 | $allSources = @() + $atFunctions + $atScripts + $extendedCommands
111 |
112 | $allSources = $allSources | Select-Object -Unique
113 | #endregion Discover Event Sources
114 | }
115 |
116 | process {
117 | foreach ($src in $allSources) {
118 | if ($Name) {
119 |
120 | $ok =
121 | foreach ($n in $Name) {
122 | $src.Name -like "$n" -or
123 | $src.Name -replace '^@' -replace '\.ps1$' -like "$n"
124 | }
125 |
126 | if (-not $ok) {
127 | continue
128 | }
129 | }
130 |
131 |
132 |
133 | if ($Subscription -or $SourceObject) {
134 | if (-not $script:SubscriptionsByEventSource) { continue }
135 | $eventSourceKey = # Then, if the event source was a script,
136 | if ($src -is [Management.Automation.ExternalScriptInfo]) {
137 | $src.Path # the key is the path.
138 | } elseif ($src.Module) { # If it was from a module
139 | $src.Module + '\' + $eventSource.Name # it's the module qualified name.
140 | } else {
141 | $src.Name # Otherwise, it's just the function name.
142 | }
143 | if (-not $script:SubscriptionsByEventSource[$eventSourceKey]) { continue }
144 | if ($Subscription) {
145 | $script:SubscriptionsByEventSource[$eventSourceKey] |
146 | Where-Object {
147 | [Runspace]::DefaultRunspace.Events.Subscribers -contains $_
148 | }
149 | } else {
150 | if ($script:SubscriptionsByEventSource[$eventSourceKey].SourceObject) {
151 | $script:SubscriptionsByEventSource[$eventSourceKey].SourceObject
152 | } else {
153 | $jobName = $script:SubscriptionsByEventSource[$eventSourceKey].Name
154 | Get-EventSubscriber -SourceIdentifier $jobName -ErrorAction SilentlyContinue |
155 | Select-Object -ExpandProperty SourceObject -ErrorAction SilentlyContinue
156 | }
157 |
158 | }
159 | continue
160 | }
161 |
162 |
163 |
164 | $src.pstypenames.clear()
165 | $src.pstypenames.add('OnQ.EventSource')
166 | if ($Help -and -not $Parameter) {
167 | Get-Help $src.EventSourceID -Full
168 | continue
169 | }
170 | $src
171 | }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 James Brundage
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/OnQ.azure-pipeline.yml:
--------------------------------------------------------------------------------
1 |
2 | parameters:
3 | - name: ModulePath
4 | type: string
5 | default:
6 | - name: Recurse
7 | type: boolean
8 | default: false
9 | - name: PesterMaxVersion
10 | type: string
11 | default: '4.99.99'
12 | stages:
13 | - stage: PowerShellStaticAnalysis
14 | displayName: Static Analysis
15 | condition: succeeded()
16 | jobs:
17 | - job: PSScriptAnalyzer
18 | displayName: PSScriptAnalyzer
19 | pool:
20 | vmImage: windows-latest
21 | steps:
22 | - powershell: |
23 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
24 | Install-Module -Name PSDevOps -Repository PSGallery -Force -Scope CurrentUser
25 | Import-Module PSDevOps -Force -PassThru
26 | name: InstallPSDevOps
27 | displayName: InstallPSDevOps
28 | - powershell: |
29 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
30 | Install-Module -Name PSScriptAnalyzer -Repository PSGallery -Force -Scope CurrentUser
31 | Import-Module PSScriptAnalyzer -Force -PassThru
32 | name: InstallPSScriptAnalyzer
33 | displayName: InstallPSScriptAnalyzer
34 | - powershell: |
35 | $Parameters = @{}
36 | $Parameters.ModulePath = @'
37 | ${{parameters.ModulePath}}
38 | '@
39 | $Parameters.Recurse = @'
40 | ${{parameters.Recurse}}
41 | '@
42 | $Parameters.Recurse = $parameters.Recurse -match 'true';
43 | foreach ($k in @($parameters.Keys)) {
44 | if ([String]::IsNullOrEmpty($parameters[$k])) {
45 | $parameters.Remove($k)
46 | }
47 | }
48 | Write-Host "##[command] RunPSScriptAnalyzer $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
49 | & {param(
50 | [string]$ModulePath,
51 | [switch]$Recurse
52 | )
53 | Import-Module PSScriptAnalyzer, PSDevOps
54 | if (-not $ModulePath) { $ModulePath = '.\'}
55 | $invokeScriptAnalyzerSplat = @{Path=$ModulePath}
56 | if ($ENV:PSScriptAnalyzer_Recurse -or $Recurse) {
57 | $invokeScriptAnalyzerSplat.Recurse = $true
58 | }
59 | $result = @(Invoke-ScriptAnalyzer @invokeScriptAnalyzerSplat)
60 | $violatedRules = $result | Select-Object -ExpandProperty RuleName
61 |
62 | Write-ADOVariable -Name PSScriptAnalyzerIssueCount -Value $result.Length -IsOutput
63 | Write-ADOVariable -Name PSScriptAnalyzerRulesViolated -Value ($violatedRules -join ',') -IsOutput
64 | foreach ($r in $result) {
65 | if ('information', 'warning' -contains $r.Severity) {
66 | Write-ADOWarning -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column
67 | }
68 | elseif ($r.Severity -eq 'Error') {
69 | Write-ADOError -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column
70 | }
71 | }} @Parameters
72 | name: RunPSScriptAnalyzer
73 | displayName: RunPSScriptAnalyzer
74 | - job: ScriptCop
75 | displayName: ScriptCop
76 | pool:
77 | vmImage: windows-latest
78 | steps:
79 | - powershell: |
80 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
81 | Install-Module -Name PSDevOps -Repository PSGallery -Force -Scope CurrentUser
82 | Import-Module PSDevOps -Force -PassThru
83 | name: InstallPSDevOps
84 | displayName: InstallPSDevOps
85 | - powershell: |
86 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
87 | Install-Module -Name ScriptCop -Repository PSGallery -Force -Scope CurrentUser
88 | Import-Module ScriptCop -Force -PassThru
89 | name: InstallScriptCop
90 | displayName: InstallScriptCop
91 | - powershell: |
92 | $Parameters = @{}
93 | $Parameters.ModulePath = @'
94 | ${{parameters.ModulePath}}
95 | '@
96 | foreach ($k in @($parameters.Keys)) {
97 | if ([String]::IsNullOrEmpty($parameters[$k])) {
98 | $parameters.Remove($k)
99 | }
100 | }
101 | Write-Host "##[command] RunScriptCop $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
102 | & {param([string]$ModulePath)
103 | Import-Module ScriptCop, PSDevOps -PassThru | Out-host
104 |
105 | if (-not $ModulePath) {
106 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
107 | $ModulePath = ".\$moduleName.psd1"
108 | }
109 |
110 | if ($ModulePath -like '*PSDevOps*') {
111 | Remove-Module PSDeVOps # If running ScriptCop on PSDeVOps, we need to remove the global module first.
112 | }
113 | "Importing from $ModulePath" | Out-Host
114 | $importedModule =Import-Module $ModulePath -Force -PassThru
115 |
116 | $importedModule | Out-Host
117 |
118 | Trace-ADOCommand -Command 'Test-Command' -Parameter @{Module=$importedModule}
119 |
120 | $importedModule |
121 | Test-Command |
122 | Tee-Object -Variable scriptCopIssues |
123 | Out-Host
124 |
125 | $scriptCopIssues = @($scriptCopIssues | Sort-Object ItemWithProblem)
126 | Write-ADOVariable -Name ScriptCopIssueCount -Value $scriptCopIssues.Length -IsOutput
127 |
128 | foreach ($issue in $scriptCopIssues) {
129 | Write-ADOWarning -Message "$($issue.ItemWithProblem): $($issue.Problem)"
130 |
131 | }
132 | } @Parameters
133 | name: RunScriptCop
134 | displayName: RunScriptCop
135 |
136 | - stage: TestPowerShellCrossPlatform
137 | displayName: Test
138 | jobs:
139 | - job: Windows
140 | displayName: Windows
141 | pool:
142 | vmImage: windows-latest
143 | steps:
144 | - powershell: |
145 | $Parameters = @{}
146 | $Parameters.PesterMaxVersion = @'
147 | ${{parameters.PesterMaxVersion}}
148 | '@
149 | foreach ($k in @($parameters.Keys)) {
150 | if ([String]::IsNullOrEmpty($parameters[$k])) {
151 | $parameters.Remove($k)
152 | }
153 | }
154 | Write-Host "##[command] InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
155 | & {param(
156 | [string]
157 | $PesterMaxVersion = '4.99.99'
158 | )
159 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
160 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber
161 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters
162 | name: InstallPester
163 | displayName: InstallPester
164 | - powershell: |
165 | $Parameters = @{}
166 | $Parameters.ModulePath = @'
167 | ${{parameters.ModulePath}}
168 | '@
169 | $Parameters.PesterMaxVersion = @'
170 | ${{parameters.PesterMaxVersion}}
171 | '@
172 | foreach ($k in @($parameters.Keys)) {
173 | if ([String]::IsNullOrEmpty($parameters[$k])) {
174 | $parameters.Remove($k)
175 | }
176 | }
177 | Write-Host "##[command] RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
178 | & {<#
179 | .Synopsis
180 | Runs Pester
181 | .Description
182 | Runs Pester tests after importing a PowerShell module
183 | #>
184 | param(
185 | # The module path. If not provided, will default to the second half of the repository ID.
186 | [string]
187 | $ModulePath,
188 | # The Pester max version. By default, this is pinned to 4.99.99.
189 | [string]
190 | $PesterMaxVersion = '4.99.99'
191 | )
192 |
193 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
194 | if (-not $ModulePath) {
195 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
196 | $ModulePath = ".\$moduleName.psd1"
197 | }
198 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion | Out-Host
199 | Import-Module $ModulePath -Force -PassThru | Out-Host
200 |
201 | $Global:ErrorActionPreference = 'continue'
202 | $Global:ProgressPreference = 'silentlycontinue'
203 |
204 | $result =
205 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml `
206 | -CodeCoverage "$(Build.SourcesDirectory)\*-*.ps1" -CodeCoverageOutputFile ".\$moduleName.Coverage.xml"
207 |
208 | "##vso[task.setvariable variable=FailedCount;isoutput=true]$($result.FailedCount)",
209 | "##vso[task.setvariable variable=PassedCount;isoutput=true]$($result.PassedCount)",
210 | "##vso[task.setvariable variable=TotalCount;isoutput=true]$($result.TotalCount)" |
211 | Out-Host
212 |
213 | if ($result.FailedCount -gt 0) {
214 | foreach ($r in $result.TestResult) {
215 | if (-not $r.Passed) {
216 | "##[error]$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)"
217 | }
218 | }
219 | throw "$($result.FailedCount) tests failed."
220 | }
221 |
222 | } @Parameters
223 | name: RunPester
224 | displayName: RunPester
225 | - task: PublishTestResults@2
226 | inputs:
227 | testResultsFormat: NUnit
228 | testResultsFiles: '**/*.TestResults.xml'
229 | mergeTestResults: true
230 | condition: always()
231 | - task: PublishCodeCoverageResults@1
232 | inputs:
233 | codeCoverageTool: JaCoCo
234 | summaryFileLocation: '**/*.Coverage.xml'
235 | reportDirectory: $(System.DefaultWorkingDirectory)
236 | condition: always()
237 | - job: Linux
238 | displayName: Linux
239 | pool:
240 | vmImage: ubuntu-latest
241 | steps:
242 | - script: |
243 |
244 | curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
245 | curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | sudo tee /etc/apt/sources.list.d/microsoft.list
246 | sudo apt-get update
247 | sudo apt-get install -y powershell
248 |
249 | displayName: Install PowerShell Core
250 | - pwsh: |
251 | $Parameters = @{}
252 | $Parameters.PesterMaxVersion = @'
253 | ${{parameters.PesterMaxVersion}}
254 | '@
255 | foreach ($k in @($parameters.Keys)) {
256 | if ([String]::IsNullOrEmpty($parameters[$k])) {
257 | $parameters.Remove($k)
258 | }
259 | }
260 | Write-Host "##[command] InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
261 | & {param(
262 | [string]
263 | $PesterMaxVersion = '4.99.99'
264 | )
265 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
266 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber
267 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters
268 | name: InstallPester
269 | displayName: InstallPester
270 | - pwsh: |
271 | $Parameters = @{}
272 | $Parameters.ModulePath = @'
273 | ${{parameters.ModulePath}}
274 | '@
275 | $Parameters.PesterMaxVersion = @'
276 | ${{parameters.PesterMaxVersion}}
277 | '@
278 | foreach ($k in @($parameters.Keys)) {
279 | if ([String]::IsNullOrEmpty($parameters[$k])) {
280 | $parameters.Remove($k)
281 | }
282 | }
283 | Write-Host "##[command] RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
284 | & {<#
285 | .Synopsis
286 | Runs Pester
287 | .Description
288 | Runs Pester tests after importing a PowerShell module
289 | #>
290 | param(
291 | # The module path. If not provided, will default to the second half of the repository ID.
292 | [string]
293 | $ModulePath,
294 | # The Pester max version. By default, this is pinned to 4.99.99.
295 | [string]
296 | $PesterMaxVersion = '4.99.99'
297 | )
298 |
299 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
300 | if (-not $ModulePath) {
301 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
302 | $ModulePath = ".\$moduleName.psd1"
303 | }
304 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion | Out-Host
305 | Import-Module $ModulePath -Force -PassThru | Out-Host
306 |
307 | $Global:ErrorActionPreference = 'continue'
308 | $Global:ProgressPreference = 'silentlycontinue'
309 |
310 | $result =
311 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml `
312 | -CodeCoverage "$(Build.SourcesDirectory)\*-*.ps1" -CodeCoverageOutputFile ".\$moduleName.Coverage.xml"
313 |
314 | "##vso[task.setvariable variable=FailedCount;isoutput=true]$($result.FailedCount)",
315 | "##vso[task.setvariable variable=PassedCount;isoutput=true]$($result.PassedCount)",
316 | "##vso[task.setvariable variable=TotalCount;isoutput=true]$($result.TotalCount)" |
317 | Out-Host
318 |
319 | if ($result.FailedCount -gt 0) {
320 | foreach ($r in $result.TestResult) {
321 | if (-not $r.Passed) {
322 | "##[error]$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)"
323 | }
324 | }
325 | throw "$($result.FailedCount) tests failed."
326 | }
327 |
328 | } @Parameters
329 | name: RunPester
330 | displayName: RunPester
331 | - task: PublishTestResults@2
332 | inputs:
333 | testResultsFormat: NUnit
334 | testResultsFiles: '**/*.TestResults.xml'
335 | mergeTestResults: true
336 | condition: always()
337 | - task: PublishCodeCoverageResults@1
338 | inputs:
339 | codeCoverageTool: JaCoCo
340 | summaryFileLocation: '**/*.Coverage.xml'
341 | reportDirectory: $(System.DefaultWorkingDirectory)
342 | condition: always()
343 | - job: MacOS
344 | displayName: MacOS
345 | pool:
346 | vmImage: macos-latest
347 | steps:
348 | - script: |
349 | brew update
350 | brew tap homebrew/cask
351 | brew install --cask powershell
352 | displayName: Install PowerShell Core
353 | - pwsh: |
354 | $Parameters = @{}
355 | $Parameters.PesterMaxVersion = @'
356 | ${{parameters.PesterMaxVersion}}
357 | '@
358 | foreach ($k in @($parameters.Keys)) {
359 | if ([String]::IsNullOrEmpty($parameters[$k])) {
360 | $parameters.Remove($k)
361 | }
362 | }
363 | Write-Host "##[command] InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
364 | & {param(
365 | [string]
366 | $PesterMaxVersion = '4.99.99'
367 | )
368 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
369 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber
370 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters
371 | name: InstallPester
372 | displayName: InstallPester
373 | - pwsh: |
374 | $Parameters = @{}
375 | $Parameters.ModulePath = @'
376 | ${{parameters.ModulePath}}
377 | '@
378 | $Parameters.PesterMaxVersion = @'
379 | ${{parameters.PesterMaxVersion}}
380 | '@
381 | foreach ($k in @($parameters.Keys)) {
382 | if ([String]::IsNullOrEmpty($parameters[$k])) {
383 | $parameters.Remove($k)
384 | }
385 | }
386 | Write-Host "##[command] RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
387 | & {<#
388 | .Synopsis
389 | Runs Pester
390 | .Description
391 | Runs Pester tests after importing a PowerShell module
392 | #>
393 | param(
394 | # The module path. If not provided, will default to the second half of the repository ID.
395 | [string]
396 | $ModulePath,
397 | # The Pester max version. By default, this is pinned to 4.99.99.
398 | [string]
399 | $PesterMaxVersion = '4.99.99'
400 | )
401 |
402 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
403 | if (-not $ModulePath) {
404 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
405 | $ModulePath = ".\$moduleName.psd1"
406 | }
407 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion | Out-Host
408 | Import-Module $ModulePath -Force -PassThru | Out-Host
409 |
410 | $Global:ErrorActionPreference = 'continue'
411 | $Global:ProgressPreference = 'silentlycontinue'
412 |
413 | $result =
414 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml `
415 | -CodeCoverage "$(Build.SourcesDirectory)\*-*.ps1" -CodeCoverageOutputFile ".\$moduleName.Coverage.xml"
416 |
417 | "##vso[task.setvariable variable=FailedCount;isoutput=true]$($result.FailedCount)",
418 | "##vso[task.setvariable variable=PassedCount;isoutput=true]$($result.PassedCount)",
419 | "##vso[task.setvariable variable=TotalCount;isoutput=true]$($result.TotalCount)" |
420 | Out-Host
421 |
422 | if ($result.FailedCount -gt 0) {
423 | foreach ($r in $result.TestResult) {
424 | if (-not $r.Passed) {
425 | "##[error]$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)"
426 | }
427 | }
428 | throw "$($result.FailedCount) tests failed."
429 | }
430 |
431 | } @Parameters
432 | name: RunPester
433 | displayName: RunPester
434 | - task: PublishTestResults@2
435 | inputs:
436 | testResultsFormat: NUnit
437 | testResultsFiles: '**/*.TestResults.xml'
438 | mergeTestResults: true
439 | condition: always()
440 | - task: PublishCodeCoverageResults@1
441 | inputs:
442 | codeCoverageTool: JaCoCo
443 | summaryFileLocation: '**/*.Coverage.xml'
444 | reportDirectory: $(System.DefaultWorkingDirectory)
445 | condition: always()
446 |
447 | - stage: UpdatePowerShellGallery
448 | displayName: Update
449 | condition: and(succeeded(), in(variables['Build.SourceBranch'], 'refs/heads/master', 'refs/heads/main'))
450 | variables:
451 | - group: Gallery
452 | jobs:
453 | - job: Publish
454 | displayName: PowerShell Gallery
455 | pool:
456 | vmImage: windows-latest
457 | steps:
458 | - checkout: self
459 | clean: true
460 | persistCredentials: true
461 | - powershell: |
462 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/"
463 | $imported = Import-Module ".\$moduleName.psd1" -Force -PassThru
464 | $foundModule = Find-Module -Name $ModuleName -ErrorAction SilentlyContinue
465 | if ($foundModule.Version -ge $imported.Version) {
466 | Write-Warning "##vso[task.logissue type=warning]Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))"
467 | } else {
468 | $gk = '$(GalleryKey)'
469 | $stagingDir = '$(Build.ArtifactStagingDirectory)'
470 | $moduleTempPath = Join-Path $stagingDir $moduleName
471 |
472 | Write-Host "Staging Directory: $ModuleTempPath"
473 |
474 | $imported | Split-Path | Copy-Item -Destination $moduleTempPath -Recurse
475 | $moduleGitPath = Join-Path $moduleTempPath '.git'
476 | Write-Host "Removing .git directory"
477 | Remove-Item -Recurse -Force $moduleGitPath
478 | Write-Host "Module Files:"
479 | Get-ChildItem $moduleTempPath -Recurse
480 | Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery"
481 | Publish-Module -Path $moduleTempPath -NuGetApiKey $gk
482 | if ($?) {
483 | Write-Host "Published to Gallery"
484 | } else {
485 | Write-Host "Gallery Publish Failed"
486 | exit 1
487 | }
488 | }
489 | name: PublishPowerShellGallery
490 | displayName: PublishPowerShellGallery
491 |
492 |
493 |
--------------------------------------------------------------------------------
/OnQ.ezout.ps1:
--------------------------------------------------------------------------------
1 | #requires -Module EZOut
2 | # Install-Module EZOut or https://github.com/StartAutomating/EZOut
3 | $myFile = $MyInvocation.MyCommand.ScriptBlock.File
4 | $myModuleName = 'OnQ'
5 | $myRoot = $myFile | Split-Path
6 | Push-Location $myRoot
7 | $formatting = @(
8 | # Add your own Write-FormatView here,
9 | # or put them in a Formatting or Views directory
10 | foreach ($potentialDirectory in 'Formatting','Views') {
11 | Join-Path $myRoot $potentialDirectory |
12 | Get-ChildItem -ea ignore |
13 | Import-FormatView -FilePath {$_.Fullname}
14 | }
15 | )
16 |
17 | $destinationRoot = $myRoot
18 |
19 | if ($formatting) {
20 | $myFormatFile = Join-Path $destinationRoot "$myModuleName.format.ps1xml"
21 | $formatting | Out-FormatData -Module $MyModuleName | Set-Content $myFormatFile -Encoding UTF8
22 | }
23 |
24 | $types = @(
25 | # Add your own Write-TypeView statements here
26 | # or declare them in the 'Types' directory
27 | Join-Path $myRoot Types |
28 | Get-Item -ea ignore |
29 | Import-TypeView
30 |
31 | )
32 |
33 | if ($types) {
34 | $myTypesFile = Join-Path $destinationRoot "$myModuleName.types.ps1xml"
35 | $types | Out-TypeData | Set-Content $myTypesFile -Encoding UTF8
36 | }
37 | Pop-Location
38 |
--------------------------------------------------------------------------------
/OnQ.format.ps1xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | OnQ.EventSource
7 |
8 | OnQ.EventSource
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | $moduleName = 'OnQ'
28 | do {
29 | $lm = Get-Module -Name $moduleName -ErrorAction Ignore
30 | if (-not $lm) { continue }
31 | if ($lm.FormatPartsLoaded) { break }
32 | $wholeScript = @(foreach ($formatFilePath in $lm.exportedFormatFiles) {
33 | foreach ($partNodeName in Select-Xml -LiteralPath $formatFilePath -XPath "/Configuration/Controls/Control/Name[starts-with(., '$')]") {
34 | $ParentNode = $partNodeName.Node.ParentNode
35 | "$($ParentNode.Name)={
36 | $($ParentNode.CustomControl.CustomEntries.CustomEntry.CustomItem.ExpressionBinding.ScriptBlock)}"
37 | }
38 | }) -join [Environment]::NewLine
39 | New-Module -Name "${ModuleName}.format.ps1xml" -ScriptBlock ([ScriptBlock]::Create(($wholeScript + ';Export-ModuleMember -Variable *'))) |
40 | Import-Module -Global
41 | $onRemove = [ScriptBlock]::Create("Remove-Module '${ModuleName}.format.ps1xml'")
42 |
43 | if (-not $lm.OnRemove) {
44 | $lm.OnRemove = $onRemove
45 | } else {
46 | $lm.OnRemove = [ScriptBlock]::Create($onRemove.ToString() + '' + [Environment]::NewLine + $lm.OnRemove)
47 | }
48 | $lm | Add-Member NoteProperty FormatPartsLoaded $true -Force
49 |
50 | } while ($false)
51 |
52 |
53 | $__ = $_
54 | $ci = . {"Success"}
55 | $_ = $__
56 | if ($ci -is [string]) {
57 | $ci = . ${OnQ_setOutputStyle} $ci
58 | } else {
59 | $ci = . ${OnQ_setOutputStyle} @ci
60 | }
61 | $output = . {$_.'Name'}
62 | @($ci; $output; . ${OnQ_ClearOutputStyle}) -join ""
63 |
64 |
65 |
66 | Synopsis
67 |
68 |
69 |
70 | @(foreach ($kv in ([Management.Automation.CommandMetaData]$_).Parameters.GetEnumerator()) {
71 | @(
72 | . ${OnQ_setOutputStyle} -ForegroundColor Verbose
73 | "[$($kv.Value.ParameterType)]"
74 | . ${OnQ_ClearOutputStyle}
75 | . ${OnQ_setOutputStyle} -ForegroundColor Warning
76 | "`$$($kv.Key)"
77 | . ${OnQ_ClearOutputStyle}
78 | ) -join ''
79 | }) -join [Environment]::NewLine
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | ${OnQ_setOutputStyle}
91 |
92 |
93 |
94 |
95 |
96 | <#
97 | .Synopsis
98 | Adds style to a format output
99 | .Description
100 | Adds style information to a format output, including:
101 |
102 | * ForegroundColor
103 | * BackgroundColor
104 | * Bold
105 | * Underline
106 | .Notes
107 | Stylized Output works in two contexts at present:
108 | * Rich consoles (Windows Terminal, PowerShell.exe, Pwsh.exe) (when $host.UI.SupportsVirtualTerminal)
109 | * Web pages (Based off the presence of a $Request variable, or when $host.UI.SupportsHTML (you must add this property to $host.UI))
110 |
111 | IsFormatPart: true
112 | #>
113 | param(
114 | [string]$ForegroundColor,
115 | [string]$BackgroundColor,
116 | [switch]$Bold,
117 | [switch]$Underline,
118 | [switch]$Invert
119 | )
120 |
121 | $canUseANSI = $host.UI.SupportsVirtualTerminal
122 | $canUseHTML = $Request -or $host.UI.SupportsHTML
123 | if (-not ($canUseANSI -or $canUseHTML)) { return }
124 |
125 | $knownStreams = @{
126 | Output='';Error='BrightRed';Warning='BrightYellow';
127 | Verbose='BrightCyan';Debug='Yellow';Progress='Cyan';
128 | Success='BrightGreen';Failure='Red';Default=''}
129 | $standardColors = 'Black', 'Red', 'Green', 'Yellow', 'Blue','Magenta', 'Cyan', 'White'
130 | $brightColors = 'BrightBlack', 'BrightRed', 'BrightGreen', 'BrightYellow', 'BrightBlue','BrightMagenta', 'BrightCyan', 'BrightWhite'
131 | $n =0
132 | $cssClasses = @()
133 | $styleAttributes =
134 | @(:nextColor foreach ($hc in $ForegroundColor,$BackgroundColor) {
135 | $n++
136 | if (-not $hc) { continue }
137 | if ($hc[0] -eq [char]0x1b) {
138 | if ($canUseANSI) {
139 | $hc; continue
140 | }
141 | }
142 |
143 | $ansiStartPoint = if ($n -eq 1) { 30 } else { 40 }
144 | if ($knownStreams.ContainsKey($hc)) {
145 | $i = $brightColors.IndexOf($knownStreams[$hc])
146 | if ($canUseHTML) {
147 | $cssClasses += $hc
148 | } else {
149 | if ($i -ge 0 -and $canUseANSI) {
150 | '' + [char]0x1b + "[1;$($ansiStartPoint + $i)m"
151 | } else {
152 | $i = $standardColors.IndexOf($knownStreams[$hc])
153 | if ($i -ge 0 -and $canUseANSI) {
154 | '' + [char]0x1b + "[1;$($ansiStartPoint + $i)m"
155 | } elseif ($i -le 0 -and $canUseANSI) {
156 | '' + [char]0x1b + "[$($ansistartpoint + 8):5m"
157 | }
158 | }
159 | }
160 | continue nextColor
161 | }
162 | elseif ($standardColors -contains $hc) {
163 | for ($i = 0; $i -lt $standardColors.Count;$i++) {
164 | if ($standardColors[$i] -eq $hc) {
165 | if ($canUseANSI -and -not $canUseHTML) {
166 | '' + [char]0x1b + "[$($ansiStartPoint + $i)m"
167 | } else {
168 | $cssClasses += $standardColors[$i]
169 | }
170 | continue nextColor
171 | }
172 | }
173 | } elseif ($brightColors -contains $hc) {
174 | for ($i = 0; $i -lt $brightColors.Count;$i++) {
175 | if ($brightColors[$i] -eq $hc) {
176 | if ($canUseANSI -and -not $canUseHTML) {
177 | '' + [char]0x1b + "[1;$($ansiStartPoint + $i)m"
178 | } else {
179 | $cssClasses += $standardColors[$i]
180 | }
181 | continue nextColor
182 | }
183 | }
184 | }
185 |
186 |
187 | if ($hc -and -not $hc.StartsWith('#')) {
188 | $placesToLook=
189 | @(if ($hc.Contains('.')) {
190 | $module, $setting = $hc -split '\.', 2
191 | $theModule = Get-Module $module
192 | $theModule.PrivateData.Color,
193 | $theModule.PrivateData.Colors,
194 | $theModule.PrivateData.Colour,
195 | $theModule.PrivateData.Colours,
196 | $theModule.PrivateData.EZOut,
197 | $global:PSColors,
198 | $global:PSColours
199 | } else {
200 | $setting = $hc
201 | $moduleColorSetting = $theModule.PrivateData.PSColors.$setting
202 | })
203 |
204 | foreach ($place in $placesToLook) {
205 | if (-not $place) { continue }
206 | foreach ($propName in $setting -split '\.') {
207 | $place = $place.$propName
208 | if (-not $place) { break }
209 | }
210 | if ($place -and "$place".StartsWith('#') -and 4,7 -contains "$place".Length) {
211 | $hc = $place
212 | continue
213 | }
214 | }
215 | if (-not $hc.StartsWith -or -not $hc.StartsWith('#')) {
216 | continue
217 | }
218 | }
219 | $r,$g,$b = if ($hc.Length -eq 7) {
220 | [int]::Parse($hc[1..2]-join'', 'HexNumber')
221 | [int]::Parse($hc[3..4]-join '', 'HexNumber')
222 | [int]::Parse($hc[5..6] -join'', 'HexNumber')
223 | }elseif ($hc.Length -eq 4) {
224 | [int]::Parse($hc[1], 'HexNumber') * 16
225 | [int]::Parse($hc[2], 'HexNumber') * 16
226 | [int]::Parse($hc[3], 'HexNumber') * 16
227 | }
228 |
229 | if ($canUseHTML) {
230 | if ($n -eq 1) { "color:$hc" }
231 | elseif ($n -eq 2) { "background-color:$hc"}
232 | }
233 | elseif ($canUseANSI) {
234 | if ($n -eq 1) { [char]0x1b+"[38;2;$r;$g;${b}m" }
235 | elseif ($n -eq 2) { [char]0x1b+"[48;2;$r;$g;${b}m" }
236 | }
237 |
238 | })
239 |
240 |
241 | if ($Bold) {
242 | $styleAttributes +=
243 | if ($canUseHTML) {
244 | "font-weight:bold"
245 | }
246 | elseif ($canUseANSI)
247 | {
248 | [char]0x1b + "[1m"
249 | }
250 | }
251 |
252 | if ($Underline) {
253 | $styleAttributes +=
254 | if ($canUseHTML) {
255 | "text-decoration:underline"
256 | } elseif ($canUseANSI) {
257 | [char]0x1b + "[4m"
258 | }
259 | }
260 |
261 | if ($Invert) {
262 | $styleAttributes +=
263 | if ($canUseHTML) {
264 | "filter:invert(100%)"
265 | } elseif ($canUseANSI) {
266 | [char]0x1b + "[7m"
267 | }
268 | }
269 |
270 | if ($canUseHTML) {
271 |
272 | "<span$(
273 | if ($styleAttributes) { " style='$($styleAttributes -join ';')'"}
274 | )$(
275 | if ($cssClasses) { " class='$($cssClasses -join ' ')'"}
276 | )>"
277 | } elseif ($canUseANSI) {
278 | $styleAttributes -join ''
279 | }
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 | ${OnQ_ClearOutputStyle}
288 |
289 |
290 |
291 |
292 |
293 | <#
294 | .Synopsis
295 | Clears the output style
296 | .Description
297 | Clears ANSI output style or closes the most recent span element.
298 |
299 | ANSI stylization can be toggled off individually (for instance, to stop applying an -Underline but leave the color unchanged)
300 | .Notes
301 | IsFormatPart: true
302 | #>
303 | param(
304 | # If set, will explicitly clear ANSI Bold
305 | [switch]
306 | $Bold,
307 | # If set, will explicitly clear ANSI Underline
308 | [switch]
309 | $Underline,
310 | # If set, will explicitly clear ANSI Invert
311 | [switch]
312 | $Invert,
313 | # If set, will explicitly clear ANSI Foreground Color
314 | [switch]
315 | $ForegroundColor,
316 | # If set, will explicitly clear ANSI Background Color
317 | [switch]
318 | $BackgroundColor
319 | )
320 | @(if ($request -or $host.UI.SupportsHTML) {
321 | "</span>"
322 | } elseif ($Host.UI.SupportsVirtualTerminal) {
323 | if ($Underline) {
324 | [char]0x1b + "[24m"
325 | }
326 | if ($Bold) {
327 | [char]0x1b + "[21m"
328 | }
329 | if ($Invert) {
330 | [char]0x1b + '[27m'
331 | }
332 | if ($ForegroundColor) {
333 | [char]0x1b + '[39m'
334 | }
335 | if ($BackgroundColor) {
336 | [char]0x1b + '[49m'
337 | }
338 |
339 | if (-not ($Underline -or $Bold -or $Invert -or $ForegroundColor -or $BackgroundColor)) {
340 | [char]0x1b + '[0m'
341 | }
342 |
343 | }) -join ''
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
--------------------------------------------------------------------------------
/OnQ.psd1:
--------------------------------------------------------------------------------
1 | @{
2 | RootModule = 'OnQ.psm1'
3 | Description = 'Easy Asynchronous Event-Driven Scripting with PowerShell'
4 | ModuleVersion = '0.1.3'
5 | GUID = 'ba5ad698-89a1-4887-9511-59f9025989b1'
6 | Author = 'James Brundage'
7 | Copyright = '2021 Start-Automating'
8 | FormatsToProcess = 'OnQ.format.ps1xml'
9 | TypesToProcess = 'OnQ.types.ps1xml'
10 | AliasesToExport = '*'
11 | PrivateData = @{
12 | PSData = @{
13 | ProjectURI = 'https://github.com/StartAutomating/OnQ'
14 | LicenseURI = 'https://github.com/StartAutomating/OnQ/blob/master/LICENSE'
15 |
16 | Tags = 'OnQ', 'Events'
17 |
18 | ReleaseNotes = @'
19 | 0.1.3:
20 | ---
21 | New Event Source:
22 | * VariableSet
23 |
24 | Receive-Event now returns event most-recent to least-recent.
25 | Receive-Event now has -First and -Skip.
26 |
27 | Bugfix: On@Repeat now actually starts it's timer.
28 |
29 | 0.1.2:
30 | ---
31 | New Event Source:
32 | * UDP
33 |
34 | PowerShellAsync Event Source now allows for a -Parameter dictionaries.
35 | 0.1.1:
36 | ---
37 | New Event Sources:
38 | * HTTPResponse
39 | * PowerShellAsync
40 |
41 | New Event Source Capabilities:
42 |
43 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute.
44 | This will be called directly after the subscription is created, so as to avoid signalling too soon.
45 |
46 | 0.1:
47 | ---
48 | Initial Module Release.
49 |
50 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"})
51 | Better pipelining support for Sending events.
52 |
53 | '@
54 | }
55 |
56 |
57 | OnQ = @{
58 | 'Time' = '@Time.ps1'
59 | 'Delay' = '@Delay.ps1'
60 | 'Process' = 'EventSources/@Process.ps1'
61 | 'ModuleChanged' = 'EventSources/@ModuleChanged.ps1'
62 | 'Job' = 'EventSources/@Job.ps1'
63 | 'PowerShellAsync' = 'EventSources/@PowerShellAsync.ps1'
64 | 'HttpResponse' = 'EventSources/@HttpResponse.ps1'
65 | 'VariableSet' = 'EventSources/@VariableSet.ps1'
66 | UDP = 'EventSources/@UDP.ps1'
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/OnQ.psm1:
--------------------------------------------------------------------------------
1 | foreach ($file in Get-ChildItem -LiteralPath $PSScriptRoot -Filter *-*.ps1) {
2 | . $file.Fullname
3 | }
4 |
5 |
6 | Set-Alias -Name On -Value Watch-Event
7 | Set-Alias -Name Send -Value Send-Event
8 | Set-Alias -Name Receive -Value Receive-Event
9 |
10 |
11 | $eventSources = Get-EventSource
12 |
13 | foreach ($es in $eventSources) {
14 | Set-Alias "On@$($es.Name -replace '^@' -replace '\.ps1$')" -Value Watch-Event
15 | }
16 |
17 | Export-ModuleMember -Alias * -Function *
--------------------------------------------------------------------------------
/OnQ.tests.ps1:
--------------------------------------------------------------------------------
1 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingCmdletAliases", "",
2 | Justification="These are smart aliases and part of syntax, and auto-fixing them is destructive.")]
3 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "",
4 | Justification="Global variables are the simplest way to test event updates.")]
5 | param()
6 |
7 | describe OnQ {
8 | it 'Helps create events' {
9 | $global:WaitAHalfSecond = $false
10 | On@Delay -Wait "00:00:00.5" -Then { $global:WaitAHalfSecond = $true; "Waited half second" }
11 | foreach ($i in 1..4) {
12 | Start-Sleep -Milliseconds 250
13 | }
14 |
15 | $global:WaitAHalfSecond | Should -Be $true
16 | }
17 | it 'Has a natural syntax' {
18 | $global:WaitedABit = $false
19 | on delay -Wait '00:00:00.5' -Then { $global:WaitedABit = $true; "Waited half second"}
20 | foreach ($i in 1..4) {
21 | Start-Sleep -Milliseconds 250
22 | }
23 | $global:WaitedABit | Should -Be $true
24 | }
25 | it 'Can handle an arbitrary signal, and send that signal' {
26 | $Global:Fired = $false
27 | on MySignal -Then { $Global:Fired = $true}
28 | send MySignal
29 | Start-Sleep -Milliseconds 250
30 | $Global:Fired | Should -Be $true
31 | }
32 | it 'Can pipe in arbitrary data to Send-Event' {
33 | $randomSourceId = "sourceId$(Get-Random)"
34 | $inputChecksum = 1..3 |
35 | Send-Event -SourceIdentifier $randomSourceId -PassThru |
36 | Select-Object -ExpandProperty MessageData |
37 | Measure-Object -Sum |
38 | Select-Object -ExpandProperty Sum
39 |
40 | $inputChecksum | Should -Be (1 + 2 + 3)
41 | }
42 | it 'Can Receive-Event sent by Send-Event, and -Clear them.' {
43 | $randomSourceId = "sourceId$(Get-Random)"
44 | 1..3 |
45 | Send-Event -SourceIdentifier $randomSourceId
46 |
47 | $outputchecksum = Receive-Event -SourceIdentifier $randomSourceId -Clear |
48 | Select-Object -ExpandProperty MessageData |
49 | Measure-Object -Sum |
50 | Select-Object -ExpandProperty Sum
51 |
52 | $outputchecksum | Should -Be (1 + 2 + 3)
53 |
54 | (Receive-Event -SourceIdentifier $randomSourceId) | Should -Be $null
55 | }
56 | it 'Can get a signal when a job finishes' {
57 | $global:JobsIsDone = $false
58 | $j = Start-Job -ScriptBlock { Start-Sleep -Milliseconds 500; "done" }
59 |
60 | $j|
61 | On@Job -Then { $global:JobsIsDone = $true }
62 |
63 | do {
64 | Start-Sleep -Milliseconds 750
65 | } while ($j.JobStateInfo.State -ne 'Completed')
66 |
67 | Start-Sleep -Milliseconds 250
68 |
69 | $global:JobsIsDone | Should -Be $true
70 | }
71 |
72 | it 'Can take any piped input with an event, and will treat -SourceIdentifier as the EventName' {
73 | $MyTimer = [Timers.Timer]::new()
74 | $MyTimer | Watch-Event -SourceIdentifier Elapsed -Then { "it's time"}
75 | }
76 |
77 | it 'Can broadly signal an event by providing an empty -Then {}' {
78 | on delay "00:00:00.1" -Then {} # Signal in a tenth of a second.
79 | Start-Sleep -Milliseconds 250
80 | $eventTimestamp = Get-Event -SourceIdentifier "System.Timers.Timer.*" |
81 | Sort-Object TimeGenerated -Descending |
82 | Select-Object -First 1 -ExpandProperty TimeGenerated
83 | ([DateTime]::Now - $eventTimestamp) |
84 | Should -BeLessOrEqual ([Timespan]"00:00:01")
85 | }
86 |
87 | it 'Can receive results from event subscriptions' {
88 | on delay "00:00:00.1" -Then {1} # Signal in a tenth of a second.
89 | Start-Sleep -Milliseconds 250
90 | $receivedResults = @(Get-EventSource -Name Delay -Subscription | Receive-Event)
91 | $receivedResults.Length | Should -BeGreaterOrEqual 1
92 | }
93 |
94 |
95 | context EventSources {
96 | it 'Has a number of built-in event source scripts.' {
97 | $es = Get-EventSource
98 | $es |
99 | Select-Object -ExpandProperty Name |
100 | Should -BeLike '@*'
101 | }
102 | it 'Can clear event sources' {
103 | $esCount = @(Get-EventSubscriber).Length
104 | $activeSubCount = @(Clear-EventSource -WhatIf).Length
105 | $activeSubCount | should -BeGreaterThan 0
106 | Clear-EventSource
107 | $esCount2 = @(Get-EventSubscriber).Length
108 | $esCount | Should -BeGreaterThan $esCount2
109 | }
110 | }
111 |
112 | context BuiltInEventSources {
113 | it 'Can run Powershell asynchronously' {
114 | On@PowerShellAsync -ScriptBlock { "hello world" } -Then {}
115 | 1..4 | Start-Sleep -Milliseconds { 250 }
116 | Get-Event -SourceIdentifier PowerShell.Async.* |
117 | Sort-Object TimeGenerated -Descending |
118 | Select-Object -First 1 -ExpandProperty MessageData |
119 | Should -Be "hello world"
120 | }
121 | it 'Can get a signal when an HTTPResponse is received' {
122 | On@HttpResponse -Uri https://github.com/ -Then {}
123 | 1..4 | Start-Sleep -Milliseconds { 250 }
124 | $responseContent = Get-Event -SourceIdentifier "HttpRequest.Completed.*" |
125 | Sort-Object TimeGenerated -Descending |
126 | Select-Object -First 1 -ExpandProperty MessageData |
127 | Select-Object -ExpandProperty ResponseContent
128 | $responseContent | Should -BeLike "*
2 |
3 |
4 |
5 | OnQ.EventSource
6 |
7 |
8 | Description
9 |
10 | # From ?<PowerShell_HelpField> in Irregular (https://github.com/StartAutomating/Irregular)
11 | [Regex]::new(@'
12 | \.(?<Field>Description) # Field Start
13 | \s{0,} # Optional Whitespace
14 | (?<Content>(.|\s)+?(?=(\.\w+|\#\>))) # Anything until the next .\field or end of the comment block
15 | '@, 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Match(
16 | $this.ScriptBlock
17 | ).Groups["Content"].Value
18 |
19 |
20 |
21 |
22 | EventSourceID
23 |
24 | if ($this -is [Management.Automation.ExternalScriptInfo]) {
25 | $this.Path # the key is the path.
26 | } elseif ($this.Module) { # If it was from a module
27 | $this.Module + '\' + $this.Name # it's the module qualified name.
28 | } else {
29 | $this.Name # Otherwise, it's just the function name.
30 | }
31 |
32 |
33 |
34 |
35 | Help
36 |
37 | Get-Help $this.EventSourceId
38 |
39 |
40 |
41 |
42 | Synopsis
43 |
44 | # From ?<PowerShell_HelpField> in Irregular (https://github.com/StartAutomating/Irregular)
45 | [Regex]::new(@'
46 | \.(?<Field>Synopsis) # Field Start
47 | \s{0,} # Optional Whitespace
48 | (?<Content>(.|\s)+?(?=(\.\w+|\#\>))) # Anything until the next .\field or end of the comment block
49 | '@, 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Match(
50 | $this.ScriptBlock
51 | ).Groups["Content"].Value
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OnQ is now [Eventful](https://github.com/StartAutomating/Eventful)
2 |
--------------------------------------------------------------------------------
/Receive-Event.ps1:
--------------------------------------------------------------------------------
1 | function Receive-Event
2 | {
3 | <#
4 | .Synopsis
5 | Receives Events
6 | .Description
7 | Receives Events and output from Event Subscriptions.
8 | .Link
9 | Send-Event
10 | .Link
11 | Watch-Event
12 | .Example
13 | Get-EventSource -Subscriber | Receive-Event
14 | .Example
15 | Receive-Event -SourceIdentifier * -First 1 # Receives the most recent event with any source identifier.
16 | #>
17 | [CmdletBinding(DefaultParameterSetName='Instance')]
18 | [OutputType([PSObject], [Management.Automation.PSEventArgs])]
19 | param(
20 | # The event subscription ID.
21 | [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='SubscriptionID')]
22 | [int[]]
23 | $SubscriptionID,
24 |
25 | # The event ID.
26 | [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='EventIdentifier')]
27 | [int[]]
28 | $EventIdentifier,
29 |
30 | # The event source identifier.
31 | [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='SourceIdentifier')]
32 | [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='SubscriptionID')]
33 | [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='EventIdentifier')]
34 | [string[]]
35 | $SourceIdentifier,
36 |
37 | # If provided, will return the first N events
38 | [int]
39 | $First,
40 |
41 | # If provided, will skip the first N events.
42 | [int]
43 | $Skip,
44 |
45 | # The input object.
46 | # If the Input Object was a job, it will receive the results of the job.
47 | [Parameter(ValueFromPipeline)]
48 | [PSObject]
49 | $InputObject,
50 |
51 | # If set, will remove events from the system after they have been returned,
52 | # and will not keep results from Jobs or Event Handlers.
53 | [switch]
54 | $Clear
55 | )
56 |
57 |
58 |
59 | begin {
60 | #region Prepare Accumulation
61 | # We will accumulate the events we output in case we need to -Clear them.
62 | $accumulated = [Collections.Arraylist]::new()
63 | filter accumulate {
64 | if (-not $skip -or ($accumulated.Count -ge $skip)) {
65 | $_
66 | }
67 |
68 | if (-not $First -or ($accumulated.Count -lt ($First + $skip))) {
69 | $null = $accumulated.Add($_)
70 | }
71 | }
72 | #endregion Prepare Accumulation
73 | }
74 | process {
75 | #region Passthru Events
76 | if ($PSCmdlet.ParameterSetName -eq 'EventIdentifier' -and
77 | $_ -is [Management.Automation.PSEventArgs]) {
78 | $_ | accumulate # pass events thru and accumulate them for later.
79 | return
80 | }
81 | #endregion PassThru Events
82 | #region Receiving Events by SourceIdentifier
83 | if ($PSCmdlet.ParameterSetName -in 'SourceIdentifier', 'EventIdentifier') {
84 | :nextEvent for ($ec = $PSCmdlet.Events.ReceivedEvents.Count -1 ; $ec -ge 0; $ec--) {
85 | $evt = $PSCmdlet.Events.ReceivedEvents[$ec]
86 | if ($SourceIdentifier) {
87 | foreach ($sid in $sourceIdentifier) {
88 | if ($evt.SourceIdentifier -eq $sid -or $evt.SourceIdentifier -like $sid) {
89 | $evt | accumulate
90 | }
91 | if ($First -and $accumulated.Count -ge ($First + $Skip)) {
92 | break nextEvent
93 | }
94 | }
95 | }
96 | if ($EventIdentifier) {
97 | foreach ($eid in $EventIdentifier) {
98 | if ($evt.EventIdentifier -eq $eid) {
99 | $evt | accumulate
100 | }
101 | if ($First -and $accumulated.Count -ge ($First + $Skip)) {
102 | break nextEvent
103 | }
104 | }
105 | }
106 | }
107 | return
108 | }
109 | #endregion Receiving Events by SourceIdentifier
110 | #region Receiving Events by SubscriptionID
111 | if ($PSCmdlet.ParameterSetName -eq 'SubscriptionID') {
112 | $SubscriptionID |
113 | # Find all event subscriptions with that subscription ID
114 | Get-EventSubscriber -SubscriptionId { $_ } -ErrorAction SilentlyContinue |
115 | # that have an .Action.
116 | Where-Object { $_.Action } |
117 | # Then pipe that action to
118 | Select-Object -ExpandProperty Action |
119 | # Receive-Job (-Keep results by default unless -Clear is passed).
120 | Receive-Job -Keep:(-not $Clear)
121 | return
122 | }
123 | #endregion Receiving Events by SubscriptionID
124 |
125 | #region Receiving Events by InputObject
126 | if ($InputObject) { # If the input object was a job,
127 | if ($InputObject -is [Management.Automation.Job]) {
128 | # Receive-Job (-Keep results by default unless -Clear is passed).
129 | $InputObject | Receive-Job -Keep:(-not $Clear)
130 | } else {
131 | # Otherwise, find event subscribers
132 | Get-EventSubscriber |
133 | # whose source is this input object
134 | Where-Object Source -EQ $InputObject |
135 | # that have an .Action.
136 | Where-Object { $_.Action } |
137 | # Then pipe that action to
138 | Select-Object -ExpandProperty Action |
139 | # Receive-Job (-Keep results by default unless -Clear is passed).
140 | Receive-Job -Keep:(-not $Clear)
141 | }
142 | }
143 | #endregion Receiving Events by InputObject
144 | }
145 |
146 | end {
147 | #region -Clear accumulation (if requested)
148 | if ($accumulated.Count -and $Clear) { # If we accumulated events, and said to -Clear them,
149 | $accumulated | Remove-Event # remove those events.
150 | }
151 | #region -Clear accumulation (if requested)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/Send-Event.ps1:
--------------------------------------------------------------------------------
1 | function Send-Event
2 | {
3 | <#
4 | .Synopsis
5 | Sends Events
6 | .Description
7 | Sends Events to PowerShell.
8 |
9 | Send-Event is a wrapper for the built-in command New-Event with a few key differences:
10 | 1. It allows MessageData to be piped in
11 | 2. You can send multiple sourceidentifiers
12 | 3. It does not output by default (you must pass -PassThru)
13 | .Example
14 | 1..4 | Send-Event "Hit It"
15 | .Link
16 | New-Event
17 | .Link
18 | Watch-Event
19 | #>
20 | [OutputType([Nullable],[Management.Automation.PSEventArgs])]
21 | param(
22 | # The SourceIdentifier
23 | [Parameter(Mandatory,Position=0)]
24 | [string[]]
25 | $SourceIdentifier,
26 |
27 | # The message data
28 | [Parameter(ValueFromPipeline,Position=1)]
29 | [PSObject]
30 | $MessageData,
31 |
32 | # The sender.
33 | [Parameter(ValueFromPipelineByPropertyName,Position=2)]
34 | [PSObject]
35 | $Sender,
36 |
37 | # The event arguments.
38 | [Parameter(ValueFromPipelineByPropertyName,Position=3)]
39 | [PSObject]
40 | $EventArgs,
41 |
42 | # If set, will output the created event.
43 | [Parameter(ValueFromPipelineByPropertyName)]
44 | [switch]
45 | $PassThru
46 | )
47 |
48 |
49 | begin {
50 | # Be we start, get a reference to New-Event
51 | $newEvent = $ExecutionContext.SessionState.InvokeCommand.GetCommand('New-Event','Cmdlet')
52 | }
53 | process {
54 | #region Map New-Event Parameters
55 | $newEventParams = @{} + $PSBoundParameters # Copy bound parameters.
56 | foreach ($k in @($newEventParams.Keys)) {
57 | if (-not $newEvent.Parameters[$k]) { # If a parameter isn't for New-Event
58 | $newEventParams.Remove($k) # remove it from the copy.
59 | }
60 | }
61 | # If we're piping in MessageData, but the message only contains "Sender" and "Event"
62 | if ($newEventParams.Sender -and $newEventParams.EventArgs -and
63 | $newEventParams.MessageData.psobject.properties.Count -eq 2 -and
64 | $newEventParams.MessageData.Sender -and $newEventParams.MessageData.EventArgs) {
65 | $newEventParams.Remove('MessageData') # remove the MessageData.
66 | }
67 | # Always remove the sourceID parameter (New-Event allows one, Send-Event allows many)
68 | $newEventParams.Remove('SourceIdentifier')
69 | #endregion Map New-Event Parameters
70 |
71 | #region Send Each Event
72 | foreach ($sourceID in $SourceIdentifier) { # Walk over each source identifier
73 | # and call New-Event.
74 | $evt = New-Event @newEventParams -SourceIdentifier $sourceID
75 | if ($PassThru) { # If we want to -PassThru events
76 | $evt # output the created event.
77 | }
78 | }
79 | #endregion Send Each Event
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Types/OnQ.EventSource/get_Description.ps1:
--------------------------------------------------------------------------------
1 | # From ? in Irregular (https://github.com/StartAutomating/Irregular)
2 | [Regex]::new(@'
3 | \.(?Description) # Field Start
4 | \s{0,} # Optional Whitespace
5 | (?(.|\s)+?(?=(\.\w+|\#\>))) # Anything until the next .\field or end of the comment block
6 | '@, 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Match(
7 | $this.ScriptBlock
8 | ).Groups["Content"].Value
9 |
--------------------------------------------------------------------------------
/Types/OnQ.EventSource/get_EventSourceID.ps1:
--------------------------------------------------------------------------------
1 | if ($this -is [Management.Automation.ExternalScriptInfo]) {
2 | $this.Path # the key is the path.
3 | } elseif ($this.Module) { # If it was from a module
4 | $this.Module + '\' + $this.Name # it's the module qualified name.
5 | } else {
6 | $this.Name # Otherwise, it's just the function name.
7 | }
8 |
--------------------------------------------------------------------------------
/Types/OnQ.EventSource/get_Help.ps1:
--------------------------------------------------------------------------------
1 | Get-Help $this.EventSourceId
2 |
--------------------------------------------------------------------------------
/Types/OnQ.EventSource/get_Synopsis.ps1:
--------------------------------------------------------------------------------
1 | # From ? in Irregular (https://github.com/StartAutomating/Irregular)
2 | [Regex]::new(@'
3 | \.(?Synopsis) # Field Start
4 | \s{0,} # Optional Whitespace
5 | (?(.|\s)+?(?=(\.\w+|\#\>))) # Anything until the next .\field or end of the comment block
6 | '@, 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Match(
7 | $this.ScriptBlock
8 | ).Groups["Content"].Value
9 |
--------------------------------------------------------------------------------
/Understanding_Event_Sources.md:
--------------------------------------------------------------------------------
1 | Understanding Event Sources
2 | ---------------------------
3 | Event Sources are scripts that produce events.
4 |
5 | They are generally named @NameOfSource.ps1.
6 |
7 | Events in PowerShell can be produced in two ways:
8 | * .NET Objects can produce events.
9 | * An event can be sent by PowerShell.
10 |
11 | An event source script can return any object with events,
12 | and indicate which events to subscribe to either by addding a
13 | [Diagnostics.Tracing.EventSource(Name='EventName')] attribute
14 | or by adding a noteproperty called "EventName" to the return.
15 |
16 | Event sources can be found a few places:
17 |
18 | * In the current directory
19 | * In any function whose name starts with @
20 | * In the directory where Watch-Event is defined
21 | * In the module root where Watch-Event is defined
22 | * In an .OnQ [Hashtable] within a module manifest's private data
23 |
24 | You can see the event sources currently available with:
25 |
26 | ~~~PowerShell
27 | Get-EventSource
28 | ~~~
--------------------------------------------------------------------------------
/Watch-Event.ps1:
--------------------------------------------------------------------------------
1 | function Watch-Event {
2 | <#
3 | .Synopsis
4 | Watches Events
5 | .Description
6 | Watches Events by SourceIdentifier, or using an EventSource script.
7 | .Example
8 | Watch-Event -SourceIdentifier MySignal -Then {"fire!" | Out-Host }
9 | .Example
10 | On MySignal { "fire!" | Out-host }
11 |
12 | New-Event MySignal
13 | .Link
14 | Get-EventSource
15 | .Link
16 | Register-ObjectEvent
17 | .Link
18 | Register-EngineEvent
19 | .Link
20 | Get-EventSubscriber
21 | .Link
22 | Unregister-Event
23 | #>
24 | [CmdletBinding(PositionalBinding=$false)]
25 | [OutputType([nullable],[PSObject])]
26 | param()
27 |
28 | dynamicParam {
29 | #region Handle Input Dynamically
30 | # Watch-Event is written in an unusually flexible way:
31 | # It has no real parameters, only dynamic ones.
32 | $DynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
33 |
34 | # The first step of our challenge is going to be determining the Event Source.
35 | $eventSource = $null
36 | # Since we only want to make one pass,
37 | # we also want to start tracking the maximum position we've found for a parameter.
38 | $maxPosition = -1
39 | # We also might want to know if we created the SourceIdentifier parameter yet.
40 | $SourceIDParameterCreated = $false
41 |
42 | #region Prepare to Add SourceID Parameter
43 | # Since we might add SourceID at a couple of different points in the code below,
44 | # we'll declare a [ScriptBlock] to add the source parameter whenever we want.
45 | $AddSourceIdParameter = {
46 | $SourceIDParameterCreated = $true # The -SourceIdentifier
47 | $sourceIdParameter = [Management.Automation.ParameterAttribute]::new()
48 | $sourceIdParameter.Mandatory = $true # will be mandatory
49 | $sourceIdParameter.Position = 0 # and will be first.
50 | # Add the dynamic parameter whenever this is called
51 | $DynamicParameters.Add("SourceIdentifier",
52 | [Management.Automation.RuntimeDefinedParameter]::new(
53 | "SourceIdentifier", [string[]], $sourceIdParameter))
54 | $maxPosition = 0 # and set MaxPosition to 0 for the next parameter.
55 | }
56 | #endregion Prepare to Add SourceID Parameter
57 |
58 | #region Find the Event Source and Map Dynamic Parameters
59 |
60 | #region Determine Invocation Name
61 | # In order to find the event source, we need to use information about how we were called.
62 | # This information is stored in $myInvocation, which can change. So we capture it's value right now.
63 | $myInv = $MyInvocation
64 | $InvocationName = '' # Our challenge is to determine the final $invocationName to use throughout the script.
65 |
66 |
67 | if ($myInv.InvocationName -ne $myInv.MyCommand.Name) { # If we're calling this command with an alias
68 | $invocationName = @($myInv.InvocationName -split '@', 2)[1] # the invocationname is what's after @.
69 | }
70 |
71 |
72 | if (-not $invocationName) { # If we don't have an invocation name,
73 | # we can peek at how we are being called (by looking at the $myInvocation.Line).
74 | $index = $myInv.Line.IndexOf($myInv.InvocationName)
75 |
76 | if ($index -ge 0) { # If our command is in the line,
77 | $myLine = $myInv.Line.Substring($index) # we can use some regex
78 | # to "peek" at positional parameters before the parameter even exists.
79 |
80 | # In this case, that regex is currently
81 | # the invocation name, followed by whitespace and a SourceIdentifier.
82 | if ($myLine -match "\s{0,}$($myInv.InvocationName)\s{1,}(?\w+)") {
83 |
84 | $invocationName = $Matches.SourceIdentifier
85 | # If we match, use that as the $invocationName
86 | # and add the sourceID parameter (so the positional parameter exists).
87 | . $AddSourceIdParameter
88 | }
89 | }
90 | }
91 |
92 | if (-not $InvocationName) {
93 | $InvocationName= $myInv.MyCommand.Name
94 | }
95 |
96 | #endregion Determine Invocation Name
97 | if ($invocationName -ne $myInv.MyCommand.Name) {
98 | #region Find Event Source
99 |
100 | # If we're being called with a smart alias, determine what the underlying command is.
101 | # But first, let's save a pointer to $executionContext.SessionState.InvokeCommand.GetCommand
102 | $getCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand
103 |
104 | # We want the next code to match quickly and not try too many codepaths,
105 | # so we'll run this in a [ScriptBlock]
106 | $eventSource =
107 | . {
108 |
109 |
110 | # Our first possibility is that we have an @Function or Alias.
111 | # Such a function would have to be defined using the provider syntax
112 | # (e.g. ${function:@EventSource} = {}) so this is _very_ unlikely to happen by mistake.
113 | $atFunction = $getCmd.Invoke("@$invocationName", 'Function,Alias')
114 | if ($atFunction) {
115 | if ($atFunction.ResolvedCommand) {
116 | return $atFunction.ResolvedCommand
117 | } else {
118 | return $atFunction
119 | }
120 |
121 | }
122 |
123 |
124 | # The next possibility is an @.ps1 script.
125 | $atFile = "@$($invocationName).ps1"
126 | # This could be in a few places: the current directory first,
127 | $localCmd = $getCmd.invoke(
128 | (Get-ChildItem -LiteralPath $pwd -Filter $atFile).FullName, 'ExternalScript')
129 | if ($localCmd) { return $localCmd }
130 | # then the directory this script is located in,
131 | $myRoot = $myInv.MyCommand.ScriptBlock.File | Split-Path -ErrorAction SilentlyContinue
132 | $atRootCmd = $getCmd.invoke(
133 | (Get-ChildItem -LiteralPath $myRoot -Filter $atFile).FullName, 'ExternalScript')
134 | if ($atRootCmd) {return $atRootCmd}
135 |
136 | if ($myInv.MyCommand.Module) { # then the module root
137 | $MyModuleRoot = $myInv.MyCommand.Module | Split-Path -ErrorAction SilentlyContinue
138 | if ($MyModule -ne $myRoot) { # (if different from $myroot).
139 | $atModuleRootCmd =
140 | $getCmd.Invoke(
141 | (Get-ChildItem -LiteralPath $MyModuleRoot -Filter $atFile).FullName, 'ExternalScript')
142 | if ($atModuleRootCmd) { $atModuleRootCmd; continue }
143 | }
144 |
145 |
146 |
147 | # Last, we want to look for extensions.
148 | $myModuleName = $myInv.MyCommand.Module.Name
149 | foreach ($loadedModule in Get-Module) { #
150 | if ( # If a module has a [Hashtable]PrivateData for this module
151 | $loadedModule.PrivateData.$myModuleName
152 | ) {
153 | $thisModuleRoot = [IO.Path]::GetDirectoryName($loadedModule.Path)
154 | $extensionData = $loadedModule.PrivateData.$myModuleName
155 | if ($extensionData -is [Hashtable]) { # that is a [Hashtable]
156 | foreach ($ed in $extensionData.GetEnumerator()) { # walk thru the hashtable
157 | if ($invocationName -eq $ed.Key) { # find out event source name
158 | $extensionCmd =
159 | if ($ed.Value -like '*.ps1') { # and map it to a script or command.
160 | $getCmd.Invoke(
161 | (Join-Path $thisModuleRoot $ed.Value),
162 | 'ExternalScript'
163 | )
164 | } else {
165 | $loadedModule.ExportedCommands[$ed.Value]
166 | }
167 |
168 | # If we could map it, return it before we keep looking thru modules.
169 | if ($extensionCmd) {
170 | return $extensionCmd
171 | }
172 | }
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
179 |
180 | }
181 | #endregion Find Event Source
182 | #region Map Dynamic Parameters
183 | if ($eventSource) {
184 | # We only need to map dynamic parameters if there is an event source.
185 | $eventSourceMetaData = [Management.Automation.CommandMetaData]$eventSource
186 | # Determine if we need to offset positional parameters,
187 | $positionOffset = [int]$SourceIDParameterCreated
188 | # then walk over the parameters from that event source.
189 | foreach ($kv in $eventSourceMetaData.Parameters.GetEnumerator()) {
190 |
191 | $attributes =
192 | if ($positionOffset) { # If we had to offset the position of a parameter
193 | @(foreach ($attr in $kv.value.attributes) {
194 | if ($attr -isnot [Management.Automation.ParameterAttribute] -or
195 | $attr.Position -lt 0
196 | ) {
197 | # we can passthru any non-parameter attributes and parameter attributes without position,
198 | $attr
199 | } else {
200 | # but parameter attributes with position need to copied and offset.
201 | $attrCopy = [Management.Automation.ParameterAttribute]::new()
202 | # (Side note: without a .Clone, copying is tedious.)
203 | foreach ($prop in $attrCopy.GetType().GetProperties('Instance,Public')) {
204 | if (-not $prop.CanWrite) { continue }
205 | if ($null -ne $attr.($prop.Name)) {
206 | $attrCopy.($prop.Name) = $attr.($prop.Name)
207 | }
208 | }
209 |
210 |
211 | # Once we have a parameter copy, offset it's position.
212 | $attrCopy.Position+=$positionOffset
213 | $pos = $attrCopy.Position
214 | $attrCopy
215 | }
216 | })
217 | } else {
218 | $pos = foreach ($a in $kv.value.attributes) {
219 | if ($a.position -ge 0) { $a.position ; break }
220 | }
221 | $kv.Value.Attributes
222 | }
223 |
224 | # Add the parameter and it's potentially modified attributes.
225 | $DynamicParameters.Add($kv.Key,
226 | [Management.Automation.RuntimeDefinedParameter]::new(
227 | $kv.Value.Name, $kv.Value.ParameterType, $attributes
228 | )
229 | )
230 | # If the parameter position was bigger than maxPosition, update maxPosition.
231 | if ($pos -ge 0 -and $pos -gt $maxPosition) { $maxPosition = $pos }
232 | }
233 | }
234 | #endregion Map Dynamic Parameters
235 | }
236 |
237 | #endregion Find the Event Source and Map Dynamic Parameters
238 |
239 |
240 | #region Add Common Parameters
241 |
242 | # If we don't have an Event Source at this point and we haven't already,
243 | if (-not $eventSource -and -not $SourceIDParameterCreated) { # add the SourceIdentifier parameter.
244 | . $addSourceIdParameter
245 | }
246 |
247 | # Also, if we don't have an event source
248 | if (-not $eventSource) {
249 | # then we can add an InputObject parameter.
250 | $inputObjectParameter = [Management.Automation.ParameterAttribute]::new()
251 | $inputObjectParameter.ValueFromPipeline = $true
252 | $DynamicParameters.Add("InputObject",
253 | [Management.Automation.RuntimeDefinedParameter]::new(
254 | "InputObject", [PSObject], $inputObjectParameter
255 | )
256 | )
257 |
258 | }
259 |
260 | # All calls will always have two additional parameters:
261 | $maxPosition++
262 | $thenParam = [Management.Automation.ParameterAttribute]::new()
263 | $thenParam.Mandatory = $true #* [ScriptBlock]$then
264 | $thenParam.Position = $maxPosition
265 | $thenActionAlias = [Management.Automation.AliasAttribute]::new("Action")
266 | $DynamicParameters.Add("Then",
267 | [Management.Automation.RuntimeDefinedParameter]::new(
268 | "Then", [ScriptBlock], @($thenParam, $thenActionAlias)
269 | )
270 | )
271 | $maxPosition++
272 | $WhenParam = [Management.Automation.ParameterAttribute]::new()
273 | $whenParam.Position = $maxPosition #* [ScriptBlock]$then
274 | $DynamicParameters.Add("When",
275 | [Management.Automation.RuntimeDefinedParameter]::new(
276 | "When", [ScriptBlock], $WhenParam
277 | )
278 | )
279 |
280 | #endregion Add Common Parameters
281 |
282 |
283 | # Now that we've got all of the dynamic parameters ready
284 | $DynamicParameterNames = $DynamicParameters.Keys -as [string[]]
285 | return $DynamicParameters # return them.
286 | #endregion Handle Input Dynamically
287 | }
288 |
289 |
290 |
291 | process {
292 |
293 | $in = $_
294 | $registerCmd = $null
295 | $registerParams = @{}
296 | $parameterCopy = @{} + $PSBoundParameters
297 | if ($DebugPreference -ne 'silentlycontinue') {
298 | Write-Debug @"
299 | Watch-Event:
300 | Dynamic Parameters: $DynamicParameterNames
301 | Bound Parameters:
302 | $($parameterCopy | Out-String)
303 | "@
304 | }
305 |
306 | #region Run Event Source and Map Parameters
307 | if ($eventSource) { # If we have an Event Source, now's the time to run it.
308 | $eventSourceParameter = [Ordered]@{} + $PSBoundParameters # Copy whatever parameters we have
309 | $eventSourceParameter.Remove('Then') # and remove -Then,
310 | $eventSourceParameter.Remove('When') # -When,
311 | $eventSourceParameter.Remove('SourceIdentifier') # and -SourceIdentifier.
312 | $eventSourceOutput = & $eventSource @eventSourceParameter # Then run the generator.
313 | $null = $PSBoundParameters.Remove('SourceIdentifier')
314 |
315 |
316 | if (-not $eventSourceOutput) { # If it didn't output,
317 | # we're gonna assume it it's gonna by signal by name.
318 | # Set it up so that later code will subscribe to this SourceIdentifier.
319 | $PSBoundParameters["SourceIdentifier"] = $eventSource.Name -replace
320 | '^@' -replace '\.event\.ps1$' -replace '\.ps1$'
321 | }
322 | elseif ($eventSourceOutput.SourceIdentifier)
323 | {
324 |
325 | # If the eventSource said what SourceIdentifier(s) it will send, we will listen.
326 | $PSBoundParameters["SourceIdentifier"] = $eventSourceOutput.SourceIdentifier
327 | } else {
328 | # Otherwise, let's see if the eventSource returned an eventName
329 | $eventName = $eventSourceOutput.EventName
330 |
331 | if (-not $eventName) { # If it didn't,
332 | $eventName =
333 | # Look at the generator script block's attibutes
334 | foreach ($attr in $eventSource.ScriptBlock.Attributes) {
335 | if ($attr.TypeId.Name -eq 'EventSourceAttribute') {
336 | # Return any [Diagnostics.Tracing.EventSource(Name='Value')]
337 | $attr.Name
338 | }
339 | }
340 | }
341 |
342 |
343 | if (-not $eventName) { # If we still don't have an event name.
344 | # check the output for events.
345 | $eventNames = @(foreach ($prop in $eventSourceOutput.psobject.members) {
346 | if ($prop.MemberType -eq 'event') {
347 | $prop.Name
348 | }
349 | })
350 |
351 | # If there was more than one
352 | if ($eventNames.Count -gt 1) {
353 | # Error out (but output the generator's output, in case that helps).
354 | $eventSourceOutput
355 | Write-Error "Source produced an object with multiple events, but did not specify a '[Diagnostics.Tracing.EventSource(Name=)]'."
356 | return
357 | }
358 |
359 |
360 | $eventName = $eventNames[0]
361 | }
362 |
363 | # Default the Register- command to Register-ObjectEvent.
364 | $registerCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Register-ObjectEvent','Cmdlet')
365 | $registerParams['InputObject']= $eventSourceOutput # Map the InputObject
366 | $registerParams['EventName'] = $eventName # and the EventName.
367 | }
368 | }
369 | #endregion Run Event Source and Map Parameters
370 |
371 | #region Handle -SourceIdentifier and -InputObject
372 | if ($PSBoundParameters['SourceIdentifier']) { # If we have a -SourceIdentifier
373 | if ($PSBoundParameters['InputObject'] -and -not $eventSource) { # and an -InputObject (but not not an eventsource)
374 | # then the register command is Register-ObjectEvent.
375 | $registerCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Register-ObjectEvent','Cmdlet')
376 |
377 | # We map our -SourceIdentifier to Register-ObjectEvent's -EventName,
378 | $registerParams['EventName'] = $PSBoundParameters['SourceIdentifier']
379 | # and Register-ObjectEvent's InputObject to -InputObject
380 | $registerParams['InputObject'] = $PSBoundParameters['InputObject']
381 | }
382 | else # If we have a -SourceIdentifier, but no -InputObject
383 | {
384 | # the register command is Register-EngineEvent.
385 | $registerCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Register-EngineEvent','Cmdlet')
386 | # and we map our -SourceIdentifier parameter to Register-EngineEvent's -SourceIdentifier.
387 | $registerParams['SourceIdentifier'] = $PSBoundParameters['SourceIdentifier']
388 | }
389 | }
390 | #endregion Handle -SourceIdentifier and -InputObject
391 |
392 | #region Handle When and Then
393 | # Assign When and Then for simplicity.
394 | $Then = $PSBoundParameters['Then']
395 | $when = $PSBoundParameters['When']
396 |
397 | if ($When) { # If -When was provided
398 | if ($when -is [ScriptBlock]) { # and it was a script
399 | # Rewrite -Then to include -When.
400 | # Run -When in a subexpression so it can return from the event handler (not itself)
401 | $then = [ScriptBlock]::Create(@"
402 | `$shouldHandle = `$(
403 | $when
404 | )
405 | if (-not `$shouldHandle) { return }
406 | $then
407 | "@)
408 | }
409 | }
410 |
411 | if ("$then" -replace '\s') { # If -Then is not blank.
412 | $registerParams["Action"] = $Then # this maps to the -Action parameter the Register- comamnd.
413 | }
414 | #endregion Handle When and Then
415 |
416 | #region Subscribe to Event
417 |
418 | # Now we create the event subscription.
419 | $eventSubscription =
420 |
421 |
422 | # If there's a Register- command.
423 | if ($registerCmd) {
424 | if ($registerParams["EventName"]) { # and we have -EventName
425 | $evtNames = $registerParams["EventName"]
426 | $registerParams.Remove('EventName')
427 |
428 | # Call Register-Object event once
429 | foreach ($evtName in $evtNames) { # for each event name (
430 | # give it a logical SourceIdentifier.
431 | $sourceId = $registerParams["InputObject"].GetType().FullName + ".$evtName"
432 | $existingSubscribers = @(Get-EventSubscriber -SourceIdentifier "${sourceID}*")
433 | if ($existingSubscribers) { # (If subscribers exist, increment the source ID))
434 | $maxSourceId = 0
435 | foreach ($es in $existingSubscribers) {
436 | if ($es.SourceIdentifier -match '\.\d+$') {
437 | $esId = [int]($matches.0 -replace '\.')
438 | if ($esId -gt $maxSourceId) {
439 | $maxSourceId = $esId
440 | }
441 | }
442 | }
443 | $sourceID = $sourceId + ".$($maxSourceId + 1)"
444 | }
445 |
446 | # Then call Register-ObjectEvent
447 | & $registerCmd @registerParams -EventName $evtName -SourceIdentifier $sourceId
448 | }
449 | }
450 | elseif ($registerParams["SourceIdentifier"]) #
451 | {
452 | $sourceIdList = $registerParams["SourceIdentifier"]
453 | $null = $registerParams.Remove('SourceIdentifier')
454 | # If we don't have an action, don't run anything (this will let the events "bubble up" to the runspace).
455 | if ($registerParams.Action) {
456 | # If we do have an action, call Register-Engine event with each source identifier.
457 | foreach ($sourceId in $sourceIdList) {
458 | & $registerCmd @registerParams -SourceIdentifier $sourceId
459 | }
460 | }
461 | }
462 | }
463 | #endregion Subscribe to Event
464 |
465 |
466 | #region Keep track of Subscriptions by EventSource
467 | # Before we're done, let's track what we subscribed to.
468 |
469 | if ($eventSource) {
470 | # Make sure a cache exists.
471 | if (-not $script:SubscriptionsByEventSource) {
472 | $script:SubscriptionsByEventSource = @{}
473 | }
474 | $eventSourceKey = # Then, if the event source was a script,
475 | if ($eventSource -is [Management.Automation.ExternalScriptInfo]) {
476 | $eventSource.Path # the key is the path.
477 | } elseif ($eventSource.Module) { # If it was from a module
478 | $eventSource.Module + '\' + $eventSource.Name # it's the module qualified name.
479 | } else {
480 | $eventSource.Name # Otherwise, it's just the function name.
481 | }
482 | $script:SubscriptionsByEventSource[$eventSourceKey] =
483 | if ($eventSubscription -is [Management.Automation.Job]) {
484 | Get-EventSubscriber -SourceIdentifier $eventSubscription.Name -ErrorAction SilentlyContinue
485 | } else {
486 | $eventSubscription
487 | }
488 |
489 | $eventSourceInitializeAttribute= $eventSource.ScriptBlock.Attributes |
490 | Where-Object TypeID -EQ ([ComponentModel.InitializationEventAttribute])
491 | if ($eventSourceOutput.InitializeEvent -and $eventSourceOutput.InitializeEvent -is [string]) {
492 | $eventSourceOutput.$($eventSourceOutput.InitializeEvent).Invoke()
493 | }
494 | elseif ($eventSourceOutput.InitializeEvent -and $eventSourceOutput.InitializeEvent -is [ScriptBlock]) {
495 | $this = $sender = $eventSourceOutput
496 | & ([ScriptBlock]::Create($eventSourceInitializeAttribute.EventName))
497 | }
498 | elseif ($eventSourceInitializeAttribute.EventName -match '^[\w\-]+$') {
499 | $eventSourceOutput.($eventSourceInitializeAttribute.EventName).Invoke()
500 | } else {
501 | $this = $sender = $eventSourceOutput
502 | & ([ScriptBlock]::Create($eventSourceInitializeAttribute.EventName))
503 | }
504 | }
505 |
506 | #endregion Keep track of Subscription
507 |
508 |
509 | #region Passthru if needed
510 | if ($myInv.PipelinePosition -lt $myInv.PipelineLength) { # If this is not the last step of the pipeline
511 | $in # pass down the original object. (This would let one set of arguments pipe to multiple calls)
512 | }
513 | else {
514 |
515 | }
516 | #endregion Passthru if needed
517 | }
518 | }
519 |
--------------------------------------------------------------------------------
/en-us/Understanding_Event_Sources.help.txt:
--------------------------------------------------------------------------------
1 | Understanding Event Sources
2 | ---------------------------
3 | Event Sources are scripts that produce events.
4 |
5 | They are generally named @NameOfSource.ps1.
6 |
7 | Events in PowerShell can be produced in two ways:
8 | * .NET Objects can produce events.
9 | * An event can be sent by PowerShell.
10 |
11 | An event source script can return any object with events,
12 | and indicate which events to subscribe to either by addding a
13 | [Diagnostics.Tracing.EventSource(Name='EventName')] attribute
14 | or by adding a noteproperty called "EventName" to the return.
15 |
16 | Event sources can be found a few places:
17 |
18 | * In the current directory
19 | * In any function whose name starts with @
20 | * In the directory where Watch-Event is defined
21 | * In the module root where Watch-Event is defined
22 | * In an .OnQ [Hashtable] within a module manifest's private data
23 |
24 | You can see the event sources currently available with:
25 |
26 | ~~~PowerShell
27 | Get-EventSource
28 | ~~~
--------------------------------------------------------------------------------
/en-us/about_OnQ.help.txt:
--------------------------------------------------------------------------------
1 | ### Getting Started with OnQ
2 |
3 |
4 | OnQ is a PowerShell module that helps you use events to script asynchronously.
5 |
6 | It gives you an easy syntax to describe events handlers in PowerShell, and provides a platform to build custom event sources.
7 |
8 | You can watch for events to occur with Watch-Event.
9 | Watch-Event is aliased to "On". For example:
10 |
11 | ~~~PowerShell
12 | # Run in a second
13 | Watch-Event Delay "00:00:01" { "In A Second!" | Out-Host }
14 |
15 | # Or, using the alias 'On'
16 | on delay "00:05:00" { "Five more minutes!" | Out-Host}
17 | ~~~
18 |
19 | These example use the built-in event source script 'Delay'.
20 |
21 | OnQ gives you a way to write scripts that will produce events. These are event sources.
22 |
23 | OnQ ships with several event sources. To see what event sources are available to you, run:
24 |
25 | ~~~PowerShell
26 | Get-EventSource # See event sources
27 | ~~~
28 |
29 |
30 | Each event source discovered when OnQ loads creates a smart alias that makes it easier to discover parameters, for example:
31 |
32 | ~~~PowerShell
33 | # Run in 30 seconds
34 | On@Delay "00:00:30" { "This Message Will Self-Destruct in 30 seconds" | Out-Host }
35 |
36 |
37 | # Run at 5:00 PM
38 | On@Time "5:00 PM" { "End Of Day!" | Out-Host }
39 |
40 | # Run every 2 minutes
41 | On@Repeat "00:02:00" { "Every other minute" | Out-Host }
42 |
43 | # Run whenever a file changes within the current directory
44 | On@FileChange { "Files Changed:$($event.SenderEventArgs)" | Out-Host }
45 | ~~~
46 |
47 |
48 | OnQ also allows you to handle an arbitrary signal.
49 | In the example below, we set up a handler for "MySignal", and the use the alias send to Send-Event(s).
50 |
51 | ~~~PowerShell
52 | On MySignal {"Fire $($event.MessageData)!" | Out-Host }
53 |
54 | # Send-Event can accept pipeline input for MessageData, and will not output unless -PassThru is specified.
55 | 1..3 | Send MySignal
56 | ~~~
57 |
58 | ---
59 |
60 | [Understanding Event Sources](Understanding_Event_Sources.md)
--------------------------------------------------------------------------------