├── docs ├── _config.yml ├── CNAME ├── Event-EventSource.md ├── 2021-11-02.md ├── 2022-06-08.md ├── 2022-10-18.md ├── 2022-10-23.md ├── 2023-06-10.md ├── 2021-11.md ├── 2022-06.md ├── 2022-10.md ├── 2023-06.md ├── LocationChanged-EventSource.md ├── 2021.md ├── 2022.md ├── 2023.md ├── CommandNotFound-EventSource.md ├── Job-EventSource.md ├── Assets │ └── Eventful.svg ├── Time-EventSource.md ├── VariableSet-EventSource.md ├── Get-EventHandler.md ├── Delay-EventSource.md ├── Repeat-EventSource.md ├── Understanding_Event_Sources.md ├── ModuleChanged-EventSource.md ├── _posts │ ├── 2021-11-02-Eventful-v0.1.4.md │ ├── 2022-06-08-Eventful-0.1.5.md │ ├── 2022-10-18-Eventful-0.1.6.md │ ├── 2022-10-23-Eventful-0.1.7.md │ └── 2023-06-10-Eventful-0.1.8.md ├── Import-Event.md ├── rss.xml ├── UDP-EventSource.md ├── Watch-Event.md ├── Clear-EventSource.md ├── Process-EventSource.md ├── PowerShellAsync-EventSource.md ├── README.md ├── Export-Event.md ├── HttpResponse-EventSource.md ├── FileChange-EventSource.md ├── Get-EventSource.md ├── Send-Event.md └── Receive-Event.md ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── event-source.md └── workflows │ ├── OnIssue.yml │ └── TestAndPublish.yml ├── Types ├── Eventful.EventHandler │ ├── get_Help.ps1 │ ├── get_IsSpecificEvent.ps1 │ ├── get_SourceIdentifier.ps1 │ ├── get_Name.ps1 │ ├── get_Description.ps1 │ └── get_Synopsis.ps1 └── Eventful.EventSource │ ├── get_Help.ps1 │ ├── get_EventSourceID.ps1 │ ├── get_Description.ps1 │ └── get_Synopsis.ps1 ├── EventSources ├── @Event.ps1 ├── @Job.ps1 ├── @Repeat.ps1 ├── @LocationChanged.ps1 ├── @Delay.ps1 ├── @CommandNotFound.ps1 ├── README.ps.md ├── @VariableSet.ps1 ├── @Time.ps1 ├── @Process.ps1 ├── @UDP.ps1 ├── README.md ├── @ModuleChanged.ps1 ├── @FileChange.ps1 ├── @PowerShellAsync.ps1 └── @HttpResponse.ps1 ├── Eventful.psm1 ├── Eventful.GithubWorkflow.psdevops.ps1 ├── GitHub └── Jobs │ ├── BuildEventful.psd1 │ └── RunGitPub.psd1 ├── Eventful.HelpOut.ps1 ├── Formatting ├── Eventful.EventSource.format.ps1 └── EventFul.EventHandler.format.ps1 ├── Assets └── Eventful.svg ├── Understanding_Event_Sources.md ├── en-us ├── Understanding_Event_Sources.help.txt └── about_Eventful.help.txt ├── LICENSE ├── Clear-EventSource.ps1 ├── Eventful.ezout.ps1 ├── Get-EventHandler.ps1 ├── Import-Event.ps1 ├── CHANGELOG.md ├── Eventful.PSSVG.ps1 ├── README.md ├── Eventful.psd1 ├── Send-Event.ps1 ├── Export-Event.ps1 ├── Eventful.types.ps1xml ├── Eventful.tests.ps1 ├── Receive-Event.ps1 ├── Get-EventSource.ps1 └── Watch-Event.ps1 /docs/_config.yml: -------------------------------------------------------------------------------- 1 | permalink: pretty -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | eventful.start-automating.com -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [StartAutomating] 2 | -------------------------------------------------------------------------------- /Types/Eventful.EventHandler/get_Help.ps1: -------------------------------------------------------------------------------- 1 | Get-Help $this.EventSourceId 2 | -------------------------------------------------------------------------------- /Types/Eventful.EventSource/get_Help.ps1: -------------------------------------------------------------------------------- 1 | Get-Help $this.EventSourceId 2 | -------------------------------------------------------------------------------- /Types/Eventful.EventHandler/get_IsSpecificEvent.ps1: -------------------------------------------------------------------------------- 1 | if ($this.Name -match '\.(events|handlers).ps1$') { 2 | $false 3 | } else { 4 | $true 5 | } 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /Types/Eventful.EventHandler/get_SourceIdentifier.ps1: -------------------------------------------------------------------------------- 1 | if ($this.Name -match '\.(events|handlers).ps1$') { 2 | '' 3 | } else { 4 | $this.Name -replace '^On_' -replace '\.handler' -replace '\.ps1$' 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/event-source.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Event Source 3 | about: 'Requests New Event Sources ' 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | -------------------------------------------------------------------------------- /Types/Eventful.EventHandler/get_Name.ps1: -------------------------------------------------------------------------------- 1 | $realName = $this.GetType().GetProperty("Name").GetGetMethod().Invoke($this, 'Instance,Public', $null, $null, $null) 2 | if ($realName -match '\.(events|handlers).ps1$') { 3 | $realName -replace '\.(events|handlers).ps1$' 4 | } else { 5 | $realName -replace '^On_' -replace '\.handler' -replace '\.ps1$' 6 | } 7 | 8 | -------------------------------------------------------------------------------- /Types/Eventful.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 | -------------------------------------------------------------------------------- /EventSources/@Event.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Watches for new events. 4 | .Description 5 | Watches for any new events. 6 | 7 | Subscribing to this event will preempt all other events. 8 | #> 9 | [Diagnostics.Tracing.EventSource(Name='PSEventReceived')] 10 | param() 11 | 12 | process { 13 | ,$([Runspace]::DefaultRunspace.Events.ReceivedEvents) 14 | } -------------------------------------------------------------------------------- /docs/Event-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@Event.ps1 2 | ----------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Watches for new events. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Watches for any new events. 18 | 19 | Subscribing to this event will preempt all other events. 20 | 21 | 22 | 23 | --- 24 | 25 | 26 | ### Syntax 27 | ```PowerShell 28 | EventSources/@Event.ps1 [] 29 | ``` 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Eventful.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 * -------------------------------------------------------------------------------- /Types/Eventful.EventHandler/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/Eventful.EventHandler/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 | -------------------------------------------------------------------------------- /Types/Eventful.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/Eventful.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 | -------------------------------------------------------------------------------- /docs/2021-11-02.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2021/11/02/ 3 | --- 4 | {% for post in site.posts %} 5 | {% assign currentdate = post.date | date: "%Y %m %d" %} 6 | {% assign friendlydate = post.date | date: "[%B](..) [%d](.) [%Y](../..)" %} 7 | {% if currentdate != "2021 11 02" %} 8 | {% continue %} 9 | {% endif %} 10 | {% if currentdate != date %} 11 | ## {{friendlydate}} 12 | {% assign date = currentdate %} 13 | {% endif %} 14 | * [ {{ post.title }} ]( {{ post.url }} ) 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /docs/2022-06-08.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2022/06/08/ 3 | --- 4 | {% for post in site.posts %} 5 | {% assign currentdate = post.date | date: "%Y %m %d" %} 6 | {% assign friendlydate = post.date | date: "[%B](..) [%d](.) [%Y](../..)" %} 7 | {% if currentdate != "2022 06 08" %} 8 | {% continue %} 9 | {% endif %} 10 | {% if currentdate != date %} 11 | ## {{friendlydate}} 12 | {% assign date = currentdate %} 13 | {% endif %} 14 | * [ {{ post.title }} ]( {{ post.url }} ) 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /docs/2022-10-18.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2022/10/18/ 3 | --- 4 | {% for post in site.posts %} 5 | {% assign currentdate = post.date | date: "%Y %m %d" %} 6 | {% assign friendlydate = post.date | date: "[%B](..) [%d](.) [%Y](../..)" %} 7 | {% if currentdate != "2022 10 18" %} 8 | {% continue %} 9 | {% endif %} 10 | {% if currentdate != date %} 11 | ## {{friendlydate}} 12 | {% assign date = currentdate %} 13 | {% endif %} 14 | * [ {{ post.title }} ]( {{ post.url }} ) 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /docs/2022-10-23.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2022/10/23/ 3 | --- 4 | {% for post in site.posts %} 5 | {% assign currentdate = post.date | date: "%Y %m %d" %} 6 | {% assign friendlydate = post.date | date: "[%B](..) [%d](.) [%Y](../..)" %} 7 | {% if currentdate != "2022 10 23" %} 8 | {% continue %} 9 | {% endif %} 10 | {% if currentdate != date %} 11 | ## {{friendlydate}} 12 | {% assign date = currentdate %} 13 | {% endif %} 14 | * [ {{ post.title }} ]( {{ post.url }} ) 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /docs/2023-06-10.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2023/06/10/ 3 | --- 4 | {% for post in site.posts %} 5 | {% assign currentdate = post.date | date: "%Y %m %d" %} 6 | {% assign friendlydate = post.date | date: "[%B](..) [%d](.) [%Y](../..)" %} 7 | {% if currentdate != "2023 06 10" %} 8 | {% continue %} 9 | {% endif %} 10 | {% if currentdate != date %} 11 | ## {{friendlydate}} 12 | {% assign date = currentdate %} 13 | {% endif %} 14 | * [ {{ post.title }} ]( {{ post.url }} ) 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /Eventful.GithubWorkflow.psdevops.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PSDevOps 2 | Import-BuildStep -Module Eventful 3 | New-GitHubWorkflow -Name "Analyze, Test, Tag, and Publish" -On Push, PullRequest, Demand -Job PowerShellStaticAnalysis, TestPowerShellOnLinux, TagReleaseAndPublish, BuildEventful -Environment @{ 4 | NoCoverage = $true 5 | } -OutputPath ( 6 | Join-Path $PSScriptRoot .github\workflows\TestAndPublish.yml 7 | ) 8 | 9 | New-GitHubWorkflow -On Issue, Demand -Job RunGitPub -Name OnIssueChanged -OutputPath ( 10 | Join-Path $PSScriptRoot .github\workflows\OnIssue.yml 11 | ) 12 | 13 | -------------------------------------------------------------------------------- /docs/2021-11.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2021/11/ 3 | --- 4 | {% assign currentYearMonth = "2021 11" %} 5 | {% for post in site.posts %} 6 | {% assign postYear = post.date | date: "%Y" %} 7 | {% assign postYearMonth = post.date | date: "%B [%Y](..)" %} 8 | {% assign postYM = post.date | date: "%Y %m" %} 9 | {% if postYM != currentYearMonth %} 10 | {% continue %} 11 | {% endif %} 12 | {% if hasDisplayedYearMonth != postYearMonth %} 13 | ## {{postYearMonth}} 14 | {% endif %} 15 | {% assign hasDisplayedYearMonth = postYearMonth %} 16 | * [ {{ post.title }} ]( {{ post.url }} ) 17 | {% endfor %} 18 | -------------------------------------------------------------------------------- /docs/2022-06.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2022/06/ 3 | --- 4 | {% assign currentYearMonth = "2022 06" %} 5 | {% for post in site.posts %} 6 | {% assign postYear = post.date | date: "%Y" %} 7 | {% assign postYearMonth = post.date | date: "%B [%Y](..)" %} 8 | {% assign postYM = post.date | date: "%Y %m" %} 9 | {% if postYM != currentYearMonth %} 10 | {% continue %} 11 | {% endif %} 12 | {% if hasDisplayedYearMonth != postYearMonth %} 13 | ## {{postYearMonth}} 14 | {% endif %} 15 | {% assign hasDisplayedYearMonth = postYearMonth %} 16 | * [ {{ post.title }} ]( {{ post.url }} ) 17 | {% endfor %} 18 | -------------------------------------------------------------------------------- /docs/2022-10.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2022/10/ 3 | --- 4 | {% assign currentYearMonth = "2022 10" %} 5 | {% for post in site.posts %} 6 | {% assign postYear = post.date | date: "%Y" %} 7 | {% assign postYearMonth = post.date | date: "%B [%Y](..)" %} 8 | {% assign postYM = post.date | date: "%Y %m" %} 9 | {% if postYM != currentYearMonth %} 10 | {% continue %} 11 | {% endif %} 12 | {% if hasDisplayedYearMonth != postYearMonth %} 13 | ## {{postYearMonth}} 14 | {% endif %} 15 | {% assign hasDisplayedYearMonth = postYearMonth %} 16 | * [ {{ post.title }} ]( {{ post.url }} ) 17 | {% endfor %} 18 | -------------------------------------------------------------------------------- /docs/2023-06.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2023/06/ 3 | --- 4 | {% assign currentYearMonth = "2023 06" %} 5 | {% for post in site.posts %} 6 | {% assign postYear = post.date | date: "%Y" %} 7 | {% assign postYearMonth = post.date | date: "%B [%Y](..)" %} 8 | {% assign postYM = post.date | date: "%Y %m" %} 9 | {% if postYM != currentYearMonth %} 10 | {% continue %} 11 | {% endif %} 12 | {% if hasDisplayedYearMonth != postYearMonth %} 13 | ## {{postYearMonth}} 14 | {% endif %} 15 | {% assign hasDisplayedYearMonth = postYearMonth %} 16 | * [ {{ post.title }} ]( {{ post.url }} ) 17 | {% endfor %} 18 | -------------------------------------------------------------------------------- /EventSources/@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 | } -------------------------------------------------------------------------------- /GitHub/Jobs/BuildEventful.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | "runs-on" = "ubuntu-latest" 3 | if = '${{ success() }}' 4 | steps = @( 5 | @{ 6 | name = 'Check out repository' 7 | uses = 'actions/checkout@v2' 8 | }, 9 | @{ 10 | name = 'GitLogger' 11 | uses = 'GitLogging/GitLoggerAction@main' 12 | id = 'GitLogger' 13 | }, 14 | @{ 15 | name = 'Use PSSVG Action' 16 | uses = 'StartAutomating/PSSVG@main' 17 | id = 'PSSVG' 18 | }, 19 | 'RunPipeScript', 20 | 'RunEZOut', 21 | 'RunHelpOut' 22 | ) 23 | } -------------------------------------------------------------------------------- /docs/LocationChanged-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@LocationChanged.ps1 2 | --------------------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Sends events when the directory changes. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Sends events when the PowerShell current directory changes. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Examples 25 | #### EXAMPLE 1 26 | ```PowerShell 27 | On@LocationChanged 28 | ``` 29 | 30 | #### EXAMPLE 2 31 | ```PowerShell 32 | On@LocationChanged -Then { $event | Out-Host } 33 | ``` 34 | 35 | 36 | 37 | --- 38 | 39 | 40 | ### Syntax 41 | ```PowerShell 42 | EventSources/@LocationChanged.ps1 [] 43 | ``` 44 | -------------------------------------------------------------------------------- /EventSources/@LocationChanged.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Sends events when the directory changes. 4 | .Description 5 | Sends events when the PowerShell current directory changes. 6 | .EXAMPLE 7 | On@LocationChanged 8 | .EXAMPLE 9 | On@LocationChanged -Then { $event | Out-Host } 10 | #> 11 | param() 12 | 13 | process { 14 | $global:ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = { 15 | param($LocationChangedArgs) 16 | 17 | New-Event -SourceIdentifier "PowerShell.LocationChanged" $LocationChangedArgs -Sender $ExecutionContext 18 | } 19 | [PSCustomObject]@{ 20 | SourceIdentifier = "PowerShell.LocationChanged" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/2021.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2021/ 3 | --- 4 | {% assign currentYear = "2021" %} 5 | {% for post in site.posts %} 6 | {% assign postYear = post.date | date: "%Y" %} 7 | {% assign postYearMonth = post.date | date: "[%B](%m) %Y" %} 8 | {% if postYear != currentYear %} 9 | {% continue %} 10 | {% endif %} 11 | {% if hasDisplayedYear != postYear %} 12 | ## [{{postYear}}](.) 13 | {% endif %} 14 | {% assign hasDisplayedYear = postYear %} 15 | {% if hasDisplayedYearMonth != postYearMonth %} 16 | ### {{postYearMonth}} 17 | {% endif %} 18 | {% assign hasDisplayedYearMonth = postYearMonth %} 19 | * [ {{ post.title }} ]( {{ post.url }} ) 20 | {% endfor %} 21 | -------------------------------------------------------------------------------- /docs/2022.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2022/ 3 | --- 4 | {% assign currentYear = "2022" %} 5 | {% for post in site.posts %} 6 | {% assign postYear = post.date | date: "%Y" %} 7 | {% assign postYearMonth = post.date | date: "[%B](%m) %Y" %} 8 | {% if postYear != currentYear %} 9 | {% continue %} 10 | {% endif %} 11 | {% if hasDisplayedYear != postYear %} 12 | ## [{{postYear}}](.) 13 | {% endif %} 14 | {% assign hasDisplayedYear = postYear %} 15 | {% if hasDisplayedYearMonth != postYearMonth %} 16 | ### {{postYearMonth}} 17 | {% endif %} 18 | {% assign hasDisplayedYearMonth = postYearMonth %} 19 | * [ {{ post.title }} ]( {{ post.url }} ) 20 | {% endfor %} 21 | -------------------------------------------------------------------------------- /docs/2023.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /2023/ 3 | --- 4 | {% assign currentYear = "2023" %} 5 | {% for post in site.posts %} 6 | {% assign postYear = post.date | date: "%Y" %} 7 | {% assign postYearMonth = post.date | date: "[%B](%m) %Y" %} 8 | {% if postYear != currentYear %} 9 | {% continue %} 10 | {% endif %} 11 | {% if hasDisplayedYear != postYear %} 12 | ## [{{postYear}}](.) 13 | {% endif %} 14 | {% assign hasDisplayedYear = postYear %} 15 | {% if hasDisplayedYearMonth != postYearMonth %} 16 | ### {{postYearMonth}} 17 | {% endif %} 18 | {% assign hasDisplayedYearMonth = postYearMonth %} 19 | * [ {{ post.title }} ]( {{ post.url }} ) 20 | {% endfor %} 21 | -------------------------------------------------------------------------------- /Eventful.HelpOut.ps1: -------------------------------------------------------------------------------- 1 | #require -Module HelpOut 2 | Push-Location $PSScriptRoot 3 | 4 | $EventfulLoaded = Get-Module Eventful 5 | if (-not $EventfulLoaded) { 6 | $EventfulLoaded = Get-ChildItem -Recurse -Filter "*.psd1" | Where-Object Name -like 'Eventful*' | Import-Module -Name { $_.FullName } -Force -PassThru 7 | } 8 | if ($EventfulLoaded) { 9 | "::notice title=ModuleLoaded::Eventful Loaded" | Out-Host 10 | } else { 11 | "::error:: Eventful not loaded" |Out-Host 12 | } 13 | 14 | Save-MarkdownHelp -Module Eventful -OutputPath $wikiPath -ScriptPath '@*' -ReplaceScriptName '^@', 15 | '\.ps1$' -ReplaceScriptNameWith '',"-EventSource" -SkipCommandType Alias -PassThru -IncludeTopic *.help.txt 16 | 17 | Pop-Location -------------------------------------------------------------------------------- /Formatting/Eventful.EventSource.format.ps1: -------------------------------------------------------------------------------- 1 | Write-FormatView -TypeName Eventful.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 | -------------------------------------------------------------------------------- /docs/CommandNotFound-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@CommandNotFound.ps1 2 | --------------------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Sends events when a command is not found. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Sends events when a command is not found. 18 | 19 | Handling this event can resolve any unknown command. 20 | 21 | 22 | 23 | --- 24 | 25 | 26 | ### Examples 27 | #### EXAMPLE 1 28 | ```PowerShell 29 | On@CommandNotFound 30 | ``` 31 | 32 | #### EXAMPLE 2 33 | ```PowerShell 34 | On@CommandNotFound -Then { $event | Out-Host } 35 | ``` 36 | 37 | 38 | 39 | --- 40 | 41 | 42 | ### Syntax 43 | ```PowerShell 44 | EventSources/@CommandNotFound.ps1 [] 45 | ``` 46 | -------------------------------------------------------------------------------- /EventSources/@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 | 21 | Add-Member NoteProperty EventName Elapsed -PassThru | 22 | Add-Member NoteProperty MaxTriggerCount 1 -PassThru 23 | $timer.Start() 24 | } -------------------------------------------------------------------------------- /Formatting/EventFul.EventHandler.format.ps1: -------------------------------------------------------------------------------- 1 | Write-FormatView -TypeName Eventful.EventHandler -Property Name, IsSpecificEvent, 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 | 18 | -------------------------------------------------------------------------------- /EventSources/@CommandNotFound.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Sends events when a command is not found. 4 | .Description 5 | Sends events when a command is not found. 6 | 7 | Handling this event can resolve any unknown command. 8 | .EXAMPLE 9 | On@CommandNotFound 10 | .EXAMPLE 11 | On@CommandNotFound -Then { $event | Out-Host } 12 | #> 13 | param() 14 | 15 | process { 16 | $global:ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction = { 17 | New-Event -SourceIdentifier "PowerShell.CommandNotFound" -MessageData $notFoundArgs -Sender $global:ExecutionContext -EventArguments $notFoundArgs 18 | } 19 | 20 | [PSCustomObject]@{ 21 | SourceIdentifier = "PowerShell.CommandNotFound" 22 | } 23 | } -------------------------------------------------------------------------------- /EventSources/README.ps.md: -------------------------------------------------------------------------------- 1 | This directory contains the built-in EventSources in Eventful. 2 | 3 | _Technically speaking_, EventSources can exist in any directory, as long as they are named liked `@*.ps1` and match `^@\w`. 4 | 5 | Event sources within Eventful or modules that tag Eventful will be automatically included. 6 | 7 | Watch-Event will also check the local directory for event sources. 8 | 9 | ~~~PipeScript{ 10 | $imported = Import-Module ../ -Global -PassThru 11 | 12 | [PSCustomObject]@{ 13 | Table = Get-EventSource | 14 | .Name { 15 | "[$($_.Name -replace '\.ps1$')](/docs/$($_.Name -replace '^@' -replace '\.ps1$')-EventSource.md)" 16 | } .Synopsis 17 | } 18 | } 19 | ~~~ 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/Job-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@Job.ps1 2 | --------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Watches a PowerShell Job's State. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Watches a PowerShell Job's StateChange event. 18 | 19 | This will send an event when a job finishes. 20 | 21 | 22 | 23 | --- 24 | 25 | 26 | ### Parameters 27 | #### **JobID** 28 | 29 | 30 | 31 | 32 | |Type |Required|Position|PipelineInput |Aliases| 33 | |---------|--------|--------|---------------------|-------| 34 | |`[Int32]`|true |1 |true (ByPropertyName)|ID | 35 | 36 | 37 | 38 | 39 | 40 | --- 41 | 42 | 43 | ### Syntax 44 | ```PowerShell 45 | EventSources/@Job.ps1 [-JobID] [] 46 | ``` 47 | -------------------------------------------------------------------------------- /Assets/Eventful.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @ 9 | eventful 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/Assets/Eventful.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @ 9 | eventful 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/Time-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@Time.ps1 2 | ---------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Sends an event at a specific time. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Sends an event at a specific date and time. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Examples 25 | #### EXAMPLE 1 26 | ```PowerShell 27 | On Time "5:00 PM" { "EOD!" | Out-Host } 28 | ``` 29 | 30 | 31 | 32 | --- 33 | 34 | 35 | ### Parameters 36 | #### **DateTime** 37 | 38 | The specific date and time the event will be triggered. 39 | 40 | 41 | 42 | 43 | 44 | 45 | |Type |Required|Position|PipelineInput| 46 | |------------|--------|--------|-------------| 47 | |`[DateTime]`|true |1 |false | 48 | 49 | 50 | 51 | 52 | 53 | --- 54 | 55 | 56 | ### Syntax 57 | ```PowerShell 58 | EventSources/@Time.ps1 [-DateTime] [] 59 | ``` 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/VariableSet-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@VariableSet.ps1 2 | ----------------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Watches for variable sets. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Watches for assignments to a variable. 18 | 19 | Events are sent directly after the variable is set. 20 | 21 | The -Sender is the callstack, The -MessageData is the value of the variable. 22 | 23 | 24 | 25 | --- 26 | 27 | 28 | ### Parameters 29 | #### **VariableName** 30 | 31 | The name of the variable 32 | 33 | 34 | 35 | 36 | 37 | 38 | |Type |Required|Position|PipelineInput | 39 | |----------|--------|--------|---------------------| 40 | |`[String]`|true |1 |true (ByPropertyName)| 41 | 42 | 43 | 44 | 45 | 46 | --- 47 | 48 | 49 | ### Syntax 50 | ```PowerShell 51 | EventSources/@VariableSet.ps1 [-VariableName] [] 52 | ``` 53 | -------------------------------------------------------------------------------- /EventSources/@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 | # The specific date and time the event will be triggered. 12 | [Parameter(Mandatory,Position=0,ParameterSetName='SpecificTime')] 13 | [DateTime] 14 | $DateTime 15 | ) 16 | 17 | process { 18 | if ($DateTime -lt [DateTime]::Now) { 19 | Write-Error "-DateTime '$DateTime' must be in the future" 20 | return 21 | } 22 | 23 | $timer = 24 | New-Object Timers.Timer -Property @{Interval=($DateTime - [DateTime]::Now).TotalMilliseconds;AutoReset=$false} 25 | 26 | if (-not $timer) { return } 27 | $timer.Start() 28 | $timer | 29 | Add-Member NoteProperty MaxTriggerCount 1 -PassThru 30 | } 31 | -------------------------------------------------------------------------------- /docs/Get-EventHandler.md: -------------------------------------------------------------------------------- 1 | Get-EventHandler 2 | ---------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Gets Event Handlers 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Gets files that act as Event Handlers. 18 | 19 | These files can be named a few ways: 20 | 21 | * On_[EventName].ps1 / [EventName].handler.ps1 (These handle a single event) 22 | * [Name].handlers.ps1 / [Name].events.ps1 (These handle multiple events) 23 | 24 | 25 | 26 | --- 27 | 28 | 29 | ### Parameters 30 | #### **HandlerPath** 31 | 32 | The path to the handler file(s) 33 | 34 | 35 | 36 | 37 | 38 | 39 | |Type |Required|Position|PipelineInput | 40 | |------------|--------|--------|---------------------| 41 | |`[String[]]`|false |1 |true (ByPropertyName)| 42 | 43 | 44 | 45 | 46 | 47 | --- 48 | 49 | 50 | ### Syntax 51 | ```PowerShell 52 | Get-EventHandler [[-HandlerPath] ] [] 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/Delay-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@Delay.ps1 2 | ----------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Send an event after a delay. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Send an event after waiting an arbitrary [Timespan] 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Examples 25 | #### EXAMPLE 1 26 | ```PowerShell 27 | On Delay "00:00:01" -Then { "In a second!" | Out-Host } 28 | ``` 29 | 30 | 31 | 32 | --- 33 | 34 | 35 | ### Parameters 36 | #### **Wait** 37 | 38 | The amount of time to wait 39 | 40 | 41 | 42 | 43 | 44 | 45 | |Type |Required|Position|PipelineInput |Aliases | 46 | |------------|--------|--------|---------------------|------------| 47 | |`[TimeSpan]`|true |1 |true (ByPropertyName)|Delay
In| 48 | 49 | 50 | 51 | 52 | 53 | --- 54 | 55 | 56 | ### Syntax 57 | ```PowerShell 58 | EventSources/@Delay.ps1 [-Wait] [] 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/Repeat-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@Repeat.ps1 2 | ------------------------ 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Send events on repeat. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Sends events on repeat, at a given [Timespan] -Interval. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Examples 25 | #### EXAMPLE 1 26 | ```PowerShell 27 | On Interval "00:01:00" { "Every minute" | Out-Host } 28 | ``` 29 | 30 | 31 | 32 | --- 33 | 34 | 35 | ### Parameters 36 | #### **Interval** 37 | 38 | The amount of time to wait between sending events. 39 | 40 | 41 | 42 | 43 | 44 | 45 | |Type |Required|Position|PipelineInput |Aliases| 46 | |------------|--------|--------|---------------------|-------| 47 | |`[TimeSpan]`|true |1 |true (ByPropertyName)|Every | 48 | 49 | 50 | 51 | 52 | 53 | --- 54 | 55 | 56 | ### Syntax 57 | ```PowerShell 58 | EventSources/@Repeat.ps1 [-Interval] [] 59 | ``` 60 | -------------------------------------------------------------------------------- /GitHub/Jobs/RunGitPub.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | "runs-on" = "ubuntu-latest" 3 | if = '${{ success() }}' 4 | steps = @( 5 | @{ 6 | name = 'Check out repository' 7 | uses = 'actions/checkout@v2' 8 | } 9 | @{ 10 | name = 'Use GitPub Action' 11 | uses = 'StartAutomating/GitPub@main' 12 | id = 'GitPub' 13 | with = @{ 14 | TargetBranch = 'edits-$([DateTime]::Now.ToString("r").Replace(":","-").Replace(" ", ""))' 15 | CommitMessage = 'Posting with GitPub [skip ci]' 16 | PublishParameters = @' 17 | { 18 | "Get-GitPubIssue": { 19 | "Repository": '${{github.repository}}', 20 | }, 21 | "Get-GitPubRelease": { 22 | "Repository": '${{github.repository}}', 23 | }, 24 | "Publish-GitPubJekyll": { 25 | "OutputPath": "docs/_posts" 26 | } 27 | } 28 | '@ 29 | } 30 | } 31 | ) 32 | } -------------------------------------------------------------------------------- /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 .Eventful [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 | ~~~ -------------------------------------------------------------------------------- /.github/workflows/OnIssue.yml: -------------------------------------------------------------------------------- 1 | 2 | name: OnIssueChanged 3 | on: 4 | issues: 5 | workflow_dispatch: 6 | jobs: 7 | RunGitPub: 8 | runs-on: ubuntu-latest 9 | if: ${{ success() }} 10 | steps: 11 | - name: Check out repository 12 | uses: actions/checkout@v2 13 | - name: Use GitPub Action 14 | uses: StartAutomating/GitPub@main 15 | id: GitPub 16 | with: 17 | TargetBranch: edits-$([DateTime]::Now.ToString("r").Replace(":","-").Replace(" ", "")) 18 | CommitMessage: Posting with GitPub [skip ci] 19 | PublishParameters: | 20 | { 21 | "Get-GitPubIssue": { 22 | "Repository": '${{github.repository}}', 23 | }, 24 | "Get-GitPubRelease": { 25 | "Repository": '${{github.repository}}', 26 | }, 27 | "Publish-GitPubJekyll": { 28 | "OutputPath": "docs/_posts" 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /docs/Understanding_Event_Sources.md: -------------------------------------------------------------------------------- 1 | Understanding Event Sources 2 | --------------------------- 3 | 4 | Event Sources are scripts that produce events. 5 | 6 | They are generally named @NameOfSource.ps1. 7 | 8 | Events in PowerShell can be produced in two ways: 9 | * .NET Objects can produce events. 10 | * An event can be sent by PowerShell. 11 | 12 | An event source script can return any object with events, 13 | and indicate which events to subscribe to either by addding a 14 | [Diagnostics.Tracing.EventSource(Name='EventName')] attribute 15 | or by adding a noteproperty called "EventName" to the return. 16 | 17 | Event sources can be found a few places: 18 | 19 | * In the current directory 20 | * In any function whose name starts with @ 21 | * In the directory where Watch-Event is defined 22 | * In the module root where Watch-Event is defined 23 | * In an .Eventful [Hashtable] within a module manifest's private data 24 | * In a module that .Tags Eventful 25 | 26 | You can see the event sources currently available with: 27 | 28 | ~~~PowerShell 29 | Get-EventSource 30 | ~~~ 31 | -------------------------------------------------------------------------------- /en-us/Understanding_Event_Sources.help.txt: -------------------------------------------------------------------------------- 1 | Understanding Event Sources 2 | --------------------------- 3 | 4 | Event Sources are scripts that produce events. 5 | 6 | They are generally named @NameOfSource.ps1. 7 | 8 | Events in PowerShell can be produced in two ways: 9 | * .NET Objects can produce events. 10 | * An event can be sent by PowerShell. 11 | 12 | An event source script can return any object with events, 13 | and indicate which events to subscribe to either by addding a 14 | [Diagnostics.Tracing.EventSource(Name='EventName')] attribute 15 | or by adding a noteproperty called "EventName" to the return. 16 | 17 | Event sources can be found a few places: 18 | 19 | * In the current directory 20 | * In any function whose name starts with @ 21 | * In the directory where Watch-Event is defined 22 | * In the module root where Watch-Event is defined 23 | * In an .Eventful [Hashtable] within a module manifest's private data 24 | * In a module that .Tags Eventful 25 | 26 | You can see the event sources currently available with: 27 | 28 | ~~~PowerShell 29 | Get-EventSource 30 | ~~~ -------------------------------------------------------------------------------- /docs/ModuleChanged-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@ModuleChanged.ps1 2 | ------------------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Watches for Module loads and unloads. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Polls the current set of globally imported PowerShell modules. 18 | 19 | When this changes, one of two events will be generated: 20 | 21 | PowerShell.Module.Loaded will be sent when one or more modules is loaded 22 | PowerShell.Module.Unloaded will be sent when one or more modules is unloaded 23 | 24 | Only one event if each will be sent per polling interval. 25 | 26 | 27 | 28 | --- 29 | 30 | 31 | ### Parameters 32 | #### **PollingInterval** 33 | 34 | The frequency to check for a module load. 35 | 36 | 37 | 38 | 39 | 40 | 41 | |Type |Required|Position|PipelineInput| 42 | |------------|--------|--------|-------------| 43 | |`[TimeSpan]`|false |1 |false | 44 | 45 | 46 | 47 | 48 | 49 | --- 50 | 51 | 52 | ### Syntax 53 | ```PowerShell 54 | EventSources/@ModuleChanged.ps1 [[-PollingInterval] ] [] 55 | ``` 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/_posts/2021-11-02-Eventful-v0.1.4.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Eventful v0.1.4 4 | sourceURL: https://github.com/StartAutomating/Eventful/releases/tag/v0.1.4 5 | tag: release 6 | --- 7 | 0.1.4 8 | --- 9 | * Module Rebranded to Eventful. 10 | * Get-EventHandler added 11 | 12 | 0.1.3 13 | --- 14 | New Event Source: 15 | * VariableSet 16 | 17 | Receive-Event now returns event most-recent to least-recent. 18 | Receive-Event now has -First and -Skip. 19 | 20 | Bugfix: On@Repeat now actually starts it's timer. 21 | 22 | 0.1.2: 23 | --- 24 | New Event Source: 25 | * UDP 26 | 27 | PowerShellAsync Event Source now allows for a -Parameter dictionaries. 28 | 0.1.1: 29 | --- 30 | New Event Sources: 31 | * HTTPResponse 32 | * PowerShellAsync 33 | 34 | New Event Source Capabilities: 35 | 36 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute. 37 | This will be called directly after the subscription is created, so as to avoid signalling too soon. 38 | 39 | 0.1: 40 | --- 41 | Initial Module Release. 42 | 43 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"}) 44 | Better pipelining support for Sending events. 45 | 46 | -------------------------------------------------------------------------------- /Eventful.ezout.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module EZOut 2 | # Install-Module EZOut or https://github.com/StartAutomating/EZOut 3 | $myFile = $MyInvocation.MyCommand.ScriptBlock.File 4 | $myModuleName = 'Eventful' 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 | -------------------------------------------------------------------------------- /docs/Import-Event.md: -------------------------------------------------------------------------------- 1 | Import-Event 2 | ------------ 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Imports Events 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Imports Events from a file on disk. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Related Links 25 | * [Export-Event](Export-Event.md) 26 | 27 | 28 | 29 | 30 | 31 | --- 32 | 33 | 34 | ### Examples 35 | #### EXAMPLE 1 36 | ```PowerShell 37 | Import-Event .\Events.clixml 38 | ``` 39 | 40 | 41 | 42 | --- 43 | 44 | 45 | ### Parameters 46 | #### **InputPath** 47 | 48 | The input path. This file should be created using Export-Event 49 | 50 | 51 | 52 | 53 | 54 | 55 | |Type |Required|Position|PipelineInput |Aliases | 56 | |----------|--------|--------|---------------------|--------| 57 | |`[String]`|true |1 |true (ByPropertyName)|Fullname| 58 | 59 | 60 | 61 | #### **Resend** 62 | 63 | If set, will resend events. 64 | Only events sent with New-Event or Send-Event will be resent. 65 | 66 | 67 | 68 | 69 | 70 | 71 | |Type |Required|Position|PipelineInput|Aliases| 72 | |----------|--------|--------|-------------|-------| 73 | |`[Switch]`|false |named |false |Replay | 74 | 75 | 76 | 77 | 78 | 79 | --- 80 | 81 | 82 | ### Syntax 83 | ```PowerShell 84 | Import-Event [-InputPath] [-Resend] [] 85 | ``` 86 | -------------------------------------------------------------------------------- /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 | Add-Member MaxTriggerCount 1 -Force -PassThru 53 | } 54 | } -------------------------------------------------------------------------------- /docs/rss.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | --- 4 | 5 | 6 | 7 | {{ site.title | xml_escape }} 8 | {{ site.description | xml_escape }} 9 | {{ site.url }}{{ site.baseurl }}/ 10 | 11 | {{ site.time | date_to_rfc822 }} 12 | {{ site.time | date_to_rfc822 }} 13 | Jekyll v{{ jekyll.version }} 14 | {% for post in site.posts limit:1000 %} 15 | {% if post.sitemap != false %} 16 | 17 | {{ post.title | xml_escape }} 18 | {{ post.content | xml_escape }} 19 | {{ post.date | date_to_rfc822 }} 20 | {{ post.url | prepend: site.baseurl | prepend: site.url }} 21 | {{ post.url | prepend: site.baseurl | prepend: site.url }} 22 | {% for tag in post.tags %} 23 | {{ tag | xml_escape }} 24 | {% endfor %} 25 | {% for cat in post.categories %} 26 | {{ cat | xml_escape }} 27 | {% endfor %} 28 | 29 | {% endif %} 30 | {% endfor %} 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/_posts/2022-06-08-Eventful-0.1.5.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Eventful 0.1.5 4 | sourceURL: https://github.com/StartAutomating/Eventful/releases/tag/v0.1.5 5 | tag: release 6 | --- 7 | ## 0.1.5 8 | * Adding On@Event ([#2](https://github.com/StartAutomating/Eventful/issues/2)) 9 | * Send-Event support for piping existing events ([#4](https://github.com/StartAutomating/Eventful/issues/4)) 10 | * Adding /docs ([#5](https://github.com/StartAutomating/Eventful/issues/5)) 11 | --- 12 | ## 0.1.4 13 | 14 | * Module Rebranded to Eventful. 15 | * Get-EventHandler added 16 | --- 17 | ## 0.1.3 18 | New Event Source: 19 | * VariableSet 20 | 21 | Receive-Event now returns event most-recent to least-recent. 22 | Receive-Event now has -First and -Skip. 23 | 24 | Bugfix: On@Repeat now actually starts it's timer. 25 | --- 26 | ## 0.1.2 27 | New Event Source: 28 | * UDP 29 | 30 | PowerShellAsync Event Source now allows for a -Parameter dictionaries. 31 | --- 32 | ## 0.1.1 33 | New Event Sources: 34 | * HTTPResponse 35 | * PowerShellAsync 36 | --- 37 | New Event Source Capabilities: 38 | 39 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute. 40 | This will be called directly after the subscription is created, so as to avoid signalling too soon. 41 | 42 | ## 0.1 43 | Initial Module Release. 44 | 45 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"}) 46 | Better pipelining support for Sending events. 47 | --- 48 | -------------------------------------------------------------------------------- /docs/UDP-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@UDP.ps1 2 | --------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Signals on UDP 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Runs PowerShell in the background. 18 | Events are fired on the completion or failure of the PowerShell command. 19 | 20 | 21 | 22 | --- 23 | 24 | 25 | ### Parameters 26 | #### **IPAddress** 27 | 28 | The IP Address where UDP packets can originate. By default, [IPAddress]::Any. 29 | 30 | 31 | 32 | 33 | 34 | 35 | |Type |Required|Position|PipelineInput | 36 | |-------------|--------|--------|---------------------| 37 | |`[IPAddress]`|false |1 |true (ByPropertyName)| 38 | 39 | 40 | 41 | #### **Port** 42 | 43 | The Port used to listen for packets. 44 | 45 | 46 | 47 | 48 | 49 | 50 | |Type |Required|Position|PipelineInput | 51 | |---------|--------|--------|---------------------| 52 | |`[Int32]`|true |2 |true (ByPropertyName)| 53 | 54 | 55 | 56 | #### **Encoding** 57 | 58 | The encoding. If provided, packet content will be decoded. 59 | 60 | 61 | 62 | 63 | 64 | 65 | |Type |Required|Position|PipelineInput | 66 | |------------|--------|--------|---------------------| 67 | |`[Encoding]`|false |3 |true (ByPropertyName)| 68 | 69 | 70 | 71 | 72 | 73 | --- 74 | 75 | 76 | ### Syntax 77 | ```PowerShell 78 | EventSources/@UDP.ps1 [[-IPAddress] ] [-Port] [[-Encoding] ] [] 79 | ``` 80 | -------------------------------------------------------------------------------- /Get-EventHandler.ps1: -------------------------------------------------------------------------------- 1 | function Get-EventHandler 2 | { 3 | <# 4 | .Synopsis 5 | Gets Event Handlers 6 | .Description 7 | Gets files that act as Event Handlers. 8 | 9 | These files can be named a few ways: 10 | 11 | * On_[EventName].ps1 / [EventName].handler.ps1 (These handle a single event) 12 | * [Name].handlers.ps1 / [Name].events.ps1 (These handle multiple events) 13 | #> 14 | param( 15 | 16 | # The path to the handler file(s) 17 | [Parameter(ValueFromPipelineByPropertyName)] 18 | [string[]] 19 | $HandlerPath 20 | ) 21 | 22 | begin { 23 | $namingConvention = "On_(?.+)\.ps1$", "(?.+)\.handler\.ps1$", "(?.+)\.handlers\.ps1$", "(?.+)\.events\.ps1$" 24 | $namingConvention = "(?>$($namingConvention -join '|'))" 25 | } 26 | process { 27 | if (-not $HandlerPath) { # If we don't have a handler path 28 | $HandlerPath = $PWD # assume the current directory 29 | } 30 | 31 | foreach ($path in $HandlerPath) { 32 | Get-ChildItem -Path $path | 33 | Where-Object Name -Match $namingConvention | 34 | ForEach-Object { 35 | $cmd = $ExecutionContext.InvokeCommand.GetCommand($_.FullName, 'ExternalScript') 36 | $cmd.pstypenames.clear() 37 | $cmd.pstypenames.add('Eventful.EventHandler') 38 | $cmd 39 | } 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/Watch-Event.md: -------------------------------------------------------------------------------- 1 | Watch-Event 2 | ----------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Watches Events 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Watches Events by SourceIdentifier, or using an EventSource script. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Related Links 25 | * [Get-EventSource](Get-EventSource.md) 26 | 27 | 28 | 29 | * [Register-ObjectEvent](https://docs.microsoft.com/powershell/module/Microsoft.PowerShell.Utility/Register-ObjectEvent) 30 | 31 | 32 | 33 | * [Register-EngineEvent](https://docs.microsoft.com/powershell/module/Microsoft.PowerShell.Utility/Register-EngineEvent) 34 | 35 | 36 | 37 | * [Get-EventSubscriber](https://docs.microsoft.com/powershell/module/Microsoft.PowerShell.Utility/Get-EventSubscriber) 38 | 39 | 40 | 41 | * [Unregister-Event](https://docs.microsoft.com/powershell/module/Microsoft.PowerShell.Utility/Unregister-Event) 42 | 43 | 44 | 45 | 46 | 47 | --- 48 | 49 | 50 | ### Examples 51 | #### EXAMPLE 1 52 | ```PowerShell 53 | Watch-Event -SourceIdentifier MySignal -Then {"fire!" | Out-Host } 54 | ``` 55 | 56 | #### EXAMPLE 2 57 | ```PowerShell 58 | On MySignal { "fire!" | Out-host } 59 | ``` 60 | New-Event MySignal 61 | 62 | 63 | --- 64 | 65 | 66 | ### Outputs 67 | * [Nullable](https://learn.microsoft.com/en-us/dotnet/api/System.Nullable) 68 | 69 | 70 | * [Management.Automation.PSObject](https://learn.microsoft.com/en-us/dotnet/api/System.Management.Automation.PSObject) 71 | 72 | 73 | 74 | 75 | 76 | 77 | --- 78 | 79 | 80 | ### Syntax 81 | ```PowerShell 82 | Watch-Event [] 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/_posts/2022-10-18-Eventful-0.1.6.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Eventful 0.1.6 4 | sourceURL: https://github.com/StartAutomating/Eventful/releases/tag/v0.1.6 5 | tag: release 6 | --- 7 | ## 0.1.6 8 | * Adding LocationChanged event source (Fixes [#12](https://github.com/StartAutomating/Eventful/issues/12)) 9 | 10 | --- 11 | 12 | ## 0.1.5 13 | * Adding On@Event ([#2](https://github.com/StartAutomating/Eventful/issues/2)) 14 | * Send-Event support for piping existing events ([#4](https://github.com/StartAutomating/Eventful/issues/4)) 15 | * Adding /docs ([#5](https://github.com/StartAutomating/Eventful/issues/5)) 16 | 17 | --- 18 | 19 | ## 0.1.4 20 | 21 | * Module Rebranded to Eventful. 22 | * Get-EventHandler added 23 | 24 | --- 25 | 26 | ## 0.1.3 27 | New Event Source: 28 | * VariableSet 29 | 30 | Receive-Event now returns event most-recent to least-recent. 31 | Receive-Event now has -First and -Skip. 32 | 33 | Bugfix: On@Repeat now actually starts it's timer. 34 | 35 | --- 36 | 37 | ## 0.1.2 38 | New Event Source: 39 | * UDP 40 | 41 | PowerShellAsync Event Source now allows for a -Parameter dictionaries. 42 | 43 | --- 44 | 45 | ## 0.1.1 46 | New Event Sources: 47 | * HTTPResponse 48 | * PowerShellAsync 49 | 50 | --- 51 | 52 | New Event Source Capabilities: 53 | 54 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute. 55 | This will be called directly after the subscription is created, so as to avoid signalling too soon. 56 | 57 | ## 0.1 58 | Initial Module Release. 59 | 60 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"}) 61 | Better pipelining support for Sending events. 62 | 63 | --- 64 | -------------------------------------------------------------------------------- /docs/Clear-EventSource.md: -------------------------------------------------------------------------------- 1 | Clear-EventSource 2 | ----------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Clears event source subscriptions 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Clears any active subscriptions for any event source. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Related Links 25 | * [Get-EventSource](Get-EventSource.md) 26 | 27 | 28 | 29 | 30 | 31 | --- 32 | 33 | 34 | ### Examples 35 | #### EXAMPLE 1 36 | ```PowerShell 37 | Clear-EventSource 38 | ``` 39 | 40 | 41 | 42 | --- 43 | 44 | 45 | ### Parameters 46 | #### **Name** 47 | 48 | The name of the event source. 49 | 50 | 51 | 52 | 53 | 54 | 55 | |Type |Required|Position|PipelineInput | 56 | |------------|--------|--------|---------------------| 57 | |`[String[]]`|false |1 |true (ByPropertyName)| 58 | 59 | 60 | 61 | #### **WhatIf** 62 | -WhatIf is an automatic variable that is created when a command has ```[CmdletBinding(SupportsShouldProcess)]```. 63 | -WhatIf is used to see what would happen, or return operations without executing them 64 | #### **Confirm** 65 | -Confirm is an automatic variable that is created when a command has ```[CmdletBinding(SupportsShouldProcess)]```. 66 | -Confirm is used to -Confirm each operation. 67 | 68 | If you pass ```-Confirm:$false``` you will not be prompted. 69 | 70 | 71 | If the command sets a ```[ConfirmImpact("Medium")]``` which is lower than ```$confirmImpactPreference```, you will not be prompted unless -Confirm is passed. 72 | 73 | 74 | 75 | --- 76 | 77 | 78 | ### Outputs 79 | * [Nullable](https://learn.microsoft.com/en-us/dotnet/api/System.Nullable) 80 | 81 | 82 | 83 | 84 | 85 | 86 | --- 87 | 88 | 89 | ### Syntax 90 | ```PowerShell 91 | Clear-EventSource [[-Name] ] [-WhatIf] [-Confirm] [] 92 | ``` 93 | -------------------------------------------------------------------------------- /Import-Event.ps1: -------------------------------------------------------------------------------- 1 | function Import-Event 2 | { 3 | <# 4 | .SYNOPSIS 5 | Imports Events 6 | .DESCRIPTION 7 | Imports Events from a file on disk. 8 | .EXAMPLE 9 | Import-Event .\Events.clixml 10 | .LINK 11 | Export-Event 12 | #> 13 | param( 14 | # The input path. This file should be created using Export-Event 15 | [ValidateScript({ 16 | $extension = @($_ -split '\.')[-1] 17 | $importer = $ExecutionContext.SessionState.InvokeCommand.GetCommand( 18 | "Import-$Extension", 19 | "Function,Alias,Cmdlet" 20 | ) 21 | if (-not $importer) { 22 | throw "Import-$Extension does not exist. Cannot import .$Extension" 23 | } 24 | return $true 25 | })] 26 | [Parameter(Mandatory,ValueFromPipelineByPropertyName)] 27 | [Alias('Fullname')] 28 | [string] 29 | $InputPath, 30 | 31 | # If set, will resend events. 32 | # Only events sent with New-Event or Send-Event will be resent. 33 | [Alias('Replay')] 34 | [switch] 35 | $Resend 36 | ) 37 | 38 | process { 39 | if (-not (Test-Path $InputPath)) { 40 | return 41 | } 42 | 43 | $extension = @($InputPath -split '\.')[-1] 44 | $importer = $ExecutionContext.SessionState.InvokeCommand.GetCommand( 45 | "Import-$Extension", 46 | "Function,Alias,Cmdlet" 47 | ) 48 | 49 | if ($resend) { 50 | & $importer $inputPath | 51 | & { process { 52 | $evt = $_ 53 | if ($evt.SourceIdentifier) { 54 | $evt | Send-Event 55 | } 56 | $evt 57 | } } 58 | } else { 59 | & $importer $inputPath 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/Process-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@Process.ps1 2 | ------------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Watches a process. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Watches a process. 18 | 19 | If -Exit is passed, watches for process exit. 20 | 21 | If -Output is passed, watches for process output 22 | 23 | If -Error is passed, watched for process error 24 | 25 | 26 | 27 | --- 28 | 29 | 30 | ### Parameters 31 | #### **ProcessID** 32 | 33 | The process identifier 34 | 35 | 36 | 37 | 38 | 39 | 40 | |Type |Required|Position|PipelineInput |Aliases| 41 | |---------|--------|--------|---------------------|-------| 42 | |`[Int32]`|true |1 |true (ByPropertyName)|ID | 43 | 44 | 45 | 46 | #### **Exit** 47 | 48 | If set, will watch for process exit. This is the default unless -StandardError or -StandardOutput are passed. 49 | 50 | 51 | 52 | 53 | 54 | 55 | |Type |Required|Position|PipelineInput| 56 | |----------|--------|--------|-------------| 57 | |`[Switch]`|false |named |false | 58 | 59 | 60 | 61 | #### **StandardOutput** 62 | 63 | If set, will watch for new standard output. 64 | 65 | 66 | 67 | 68 | 69 | 70 | |Type |Required|Position|PipelineInput| 71 | |----------|--------|--------|-------------| 72 | |`[Switch]`|false |named |false | 73 | 74 | 75 | 76 | #### **StandardError** 77 | 78 | If set, will watch for new standard erorr. 79 | 80 | 81 | 82 | 83 | 84 | 85 | |Type |Required|Position|PipelineInput| 86 | |----------|--------|--------|-------------| 87 | |`[Switch]`|false |named |false | 88 | 89 | 90 | 91 | 92 | 93 | --- 94 | 95 | 96 | ### Syntax 97 | ```PowerShell 98 | EventSources/@Process.ps1 [-ProcessID] [-Exit] [-StandardOutput] [-StandardError] [] 99 | ``` 100 | -------------------------------------------------------------------------------- /docs/_posts/2022-10-23-Eventful-0.1.7.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Eventful 0.1.7 4 | sourceURL: https://github.com/StartAutomating/Eventful/releases/tag/v0.1.7 5 | tag: release 6 | --- 7 | ## 0.1.7 8 | * Adding On@CommandNotFound event source (Fixes [#11](https://github.com/StartAutomating/Eventful/issues/11)) 9 | * Watch-Event now allows eventsources -recursively (Fixes [#15](https://github.com/StartAutomating/Eventful/issues/15)) 10 | 11 | --- 12 | 13 | ## 0.1.6 14 | * Adding LocationChanged event source (Fixes [#12](https://github.com/StartAutomating/Eventful/issues/12)) 15 | 16 | --- 17 | 18 | ## 0.1.5 19 | * Adding On@Event ([#2](https://github.com/StartAutomating/Eventful/issues/2)) 20 | * Send-Event support for piping existing events ([#4](https://github.com/StartAutomating/Eventful/issues/4)) 21 | * Adding /docs ([#5](https://github.com/StartAutomating/Eventful/issues/5)) 22 | 23 | --- 24 | 25 | ## 0.1.4 26 | 27 | * Module Rebranded to Eventful. 28 | * Get-EventHandler added 29 | 30 | --- 31 | 32 | ## 0.1.3 33 | New Event Source: 34 | * VariableSet 35 | 36 | Receive-Event now returns event most-recent to least-recent. 37 | Receive-Event now has -First and -Skip. 38 | 39 | Bugfix: On@Repeat now actually starts it's timer. 40 | 41 | --- 42 | 43 | ## 0.1.2 44 | New Event Source: 45 | * UDP 46 | 47 | PowerShellAsync Event Source now allows for a -Parameter dictionaries. 48 | 49 | --- 50 | 51 | ## 0.1.1 52 | New Event Sources: 53 | * HTTPResponse 54 | * PowerShellAsync 55 | 56 | --- 57 | 58 | New Event Source Capabilities: 59 | 60 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute. 61 | This will be called directly after the subscription is created, so as to avoid signalling too soon. 62 | 63 | ## 0.1 64 | Initial Module Release. 65 | 66 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"}) 67 | Better pipelining support for Sending events. 68 | 69 | --- 70 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Eventful 0.1.8: 2 | 3 | * Eventful Supports Sponsorship (#25) 4 | * New Commands! 5 | * Import-Event (#28) 6 | * Export-Event (#27) 7 | * Send-Event can now send -EventArguments and -MessageData (#26) 8 | * Watch-Event now supports -MaxTriggerCount and -MessageData (#29) 9 | * Simplifying event source registration (any `@*` script or function) (#30) 10 | * Making one-time event sources more efficient (#31) 11 | 12 | --- 13 | 14 | ## Eventful 0.1.7: 15 | * Adding On@CommandNotFound event source (Fixes #11) 16 | * Watch-Event now allows eventsources -recursively (Fixes #15) 17 | 18 | --- 19 | 20 | ## Eventful 0.1.6 21 | * Adding LocationChanged event source (Fixes #12) 22 | 23 | --- 24 | 25 | ## Eventful 0.1.5 26 | * Adding On@Event (#2) 27 | * Send-Event support for piping existing events (#4) 28 | * Adding /docs (#5) 29 | 30 | --- 31 | 32 | ## Eventful 0.1.4 33 | 34 | * Module Rebranded to Eventful. 35 | * Get-EventHandler added 36 | 37 | --- 38 | 39 | ## Eventful 0.1.3 40 | New Event Source: 41 | * VariableSet 42 | 43 | Receive-Event now returns event most-recent to least-recent. 44 | Receive-Event now has -First and -Skip. 45 | 46 | Bugfix: On@Repeat now actually starts it's timer. 47 | 48 | --- 49 | 50 | ## Eventful 0.1.2 51 | New Event Source: 52 | * UDP 53 | 54 | PowerShellAsync Event Source now allows for a -Parameter dictionaries. 55 | 56 | --- 57 | 58 | ## Eventful 0.1.1 59 | New Event Sources: 60 | * HTTPResponse 61 | * PowerShellAsync 62 | 63 | --- 64 | 65 | New Event Source Capabilities: 66 | 67 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute. 68 | This will be called directly after the subscription is created, so as to avoid signalling too soon. 69 | 70 | --- 71 | 72 | ## Eventful 0.1 73 | 74 | Initial Module Release. 75 | 76 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"}) 77 | Better pipelining support for Sending events. 78 | 79 | --- -------------------------------------------------------------------------------- /docs/PowerShellAsync-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@PowerShellAsync.ps1 2 | --------------------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Runs PowerShell asynchronously 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Runs PowerShell in the background. 18 | Events are fired on the completion or failure of the PowerShell command. 19 | 20 | 21 | 22 | --- 23 | 24 | 25 | ### Parameters 26 | #### **ScriptBlock** 27 | 28 | The scripts you would like to run. Each script block will be counted as a distinct statement. 29 | 30 | 31 | 32 | 33 | 34 | 35 | |Type |Required|Position|PipelineInput| 36 | |-----------------|--------|--------|-------------| 37 | |`[ScriptBlock[]]`|true |1 |false | 38 | 39 | 40 | 41 | #### **Parameter** 42 | 43 | The named parameters passed to each script. 44 | 45 | 46 | 47 | 48 | 49 | 50 | |Type |Required|Position|PipelineInput|Aliases | 51 | |-----------------|--------|--------|-------------|----------| 52 | |`[IDictionary[]]`|false |named |false |Parameters| 53 | 54 | 55 | 56 | #### **Runspace** 57 | 58 | If provided, will run in a specified runspace. The Runspace must already be open. 59 | 60 | 61 | 62 | 63 | 64 | 65 | |Type |Required|Position|PipelineInput| 66 | |------------|--------|--------|-------------| 67 | |`[Runspace]`|false |named |false | 68 | 69 | 70 | 71 | #### **RunspacePool** 72 | 73 | If provided, will run in a runspace pool. The RunspacePool must already be open. 74 | 75 | 76 | 77 | 78 | 79 | 80 | |Type |Required|Position|PipelineInput| 81 | |----------------|--------|--------|-------------| 82 | |`[RunspacePool]`|false |named |false | 83 | 84 | 85 | 86 | 87 | 88 | --- 89 | 90 | 91 | ### Syntax 92 | ```PowerShell 93 | EventSources/@PowerShellAsync.ps1 [-ScriptBlock] [-Parameter ] [-Runspace ] [-RunspacePool ] [] 94 | ``` 95 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### Getting Started with Eventful 2 | 3 | 4 | Eventful 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 | Eventful gives you a way to write scripts that will produce events. These are event sources. 22 | 23 | Eventful 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 Eventful 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 | Eventful 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) 61 | -------------------------------------------------------------------------------- /en-us/about_Eventful.help.txt: -------------------------------------------------------------------------------- 1 | ### Getting Started with Eventful 2 | 3 | 4 | Eventful 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 | Eventful gives you a way to write scripts that will produce events. These are event sources. 22 | 23 | Eventful 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 Eventful 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 | Eventful 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) -------------------------------------------------------------------------------- /Eventful.PSSVG.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PSSVG 2 | 3 | $assetsPath = Join-Path $PSScriptRoot Assets 4 | 5 | if (-not (Test-Path $assetsPath)) { 6 | $null = New-Item -ItemType Directory -path $assetsPath -Force 7 | } 8 | = -content $( 9 | $commonParameters = [Ordered]@{ 10 | Fill = '#4488FF' 11 | Stroke = 'black' 12 | StrokeWidth = '0.05' 13 | } 14 | 15 | = -Id psChevron -Content @( 16 | = -Points (@( 17 | "40,20" 18 | "45,20" 19 | "60,50" 20 | "35,80" 21 | "32.5,80" 22 | "55,50" 23 | ) -join ' ') 24 | ) -ViewBox 100, 100 25 | 26 | 27 | = -Href '#psChevron' -X -25% -Y 35% @commonParameters -Height 30% -Opacity .9 28 | # = -Text '>' -X 20 -Y 50 -FontSize 48 -Rotate -7 -FontFamily monospace @commonParameters 29 | = -Text @( 30 | = -Content '@' -Dx -8.5em -FontFamily sans-serif -FontSize 4 -DominantBaseline 'middle' 31 | = -Content 'eventful' -TextAnchor 'left' -FontFamily monospace -Dx -.4em -DominantBaseline 'middle' -Dy 0em 32 | 33 | # = -Content '@' -Dx -8.5em -FontFamily sans-serif -FontSize 4 -DominantBaseline 'middle' -Dy -1em 34 | ) -X 40% -Y 50% -FontSize 24 -FontFamily monospace @commonParameters 35 | 36 | <#= -Text @( 37 | = -Content 'eventful' -TextAnchor 'left' -FontFamily monospace -Dx -1em 38 | ) -X 40% -Y 56% -FontSize 24 -FontFamily monospace @commonParameters 39 | = -Text @( 40 | = -Content '@' -TextAnchor 'right' -FontFamily sans-serif -Dx -1em 41 | ) -X 25% -Y 53% -FontSize 12 -FontFamily monospace @commonParameters #> 42 | # = -Text 'SVG' -X 70% -Y 53.5% -FontSize 36 -FontFamily monospace -FontStretch "10%" -FontWeight 150 @commonParameters -DominantBaseline 'middle' -TextAnchor 'middle' -Opacity .9 43 | 44 | ) -ViewBox 0, 0, 200, 100 -OutputPath $( 45 | Join-Path (Join-Path $PSScriptRoot Assets) Eventful.svg 46 | ) -------------------------------------------------------------------------------- /EventSources/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the built-in EventSources in Eventful. 2 | 3 | _Technically speaking_, EventSources can exist in any directory, as long as they are named liked `@*.ps1` and match `^@\w`. 4 | 5 | Event sources within Eventful or modules that tag Eventful will be automatically included. 6 | 7 | Watch-Event will also check the local directory for event sources. 8 | 9 | 10 | |Name |Synopsis | 11 | |--------------------------------------------------------|----------------------------------------------| 12 | |[@CommandNotFound](/docs/CommandNotFound-EventSource.md)|Sends events when a command is not found.
| 13 | |[@Delay](/docs/Delay-EventSource.md) |Send an event after a delay.
| 14 | |[@Event](/docs/Event-EventSource.md) |Watches for new events.
| 15 | |[@FileChange](/docs/FileChange-EventSource.md) |Watches for File Changes.
| 16 | |[@HttpResponse](/docs/HttpResponse-EventSource.md) |Sends events on HTTP Responses.
| 17 | |[@Job](/docs/Job-EventSource.md) |Watches a PowerShell Job's State.
| 18 | |[@LocationChanged](/docs/LocationChanged-EventSource.md)|Sends events when the directory changes.
| 19 | |[@ModuleChanged](/docs/ModuleChanged-EventSource.md) |Watches for Module loads and unloads.
| 20 | |[@PowerShellAsync](/docs/PowerShellAsync-EventSource.md)|Runs PowerShell asynchronously
| 21 | |[@Process](/docs/Process-EventSource.md) |Watches a process.
| 22 | |[@Repeat](/docs/Repeat-EventSource.md) |Send events on repeat.
| 23 | |[@Time](/docs/Time-EventSource.md) |Sends an event at a specific time.
| 24 | |[@UDP](/docs/UDP-EventSource.md) |Signals on UDP
| 25 | |[@VariableSet](/docs/VariableSet-EventSource.md) |Watches for variable sets.
| 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | Easy Eventful PowerShell 6 | ---------------- 7 | 8 | ### Getting Started with Eventful 9 | 10 | 11 | Eventful is a PowerShell module that helps you script asynchronously. 12 | 13 | It gives you an easy syntax to describe events handlers in PowerShell, and provides a platform to build custom event sources. 14 | 15 | You can watch for events to occur with Watch-Event. 16 | Watch-Event is aliased to "On". For example: 17 | 18 | ~~~PowerShell 19 | # Run in a second 20 | Watch-Event Delay "00:00:01" { "In A Second!" | Out-Host } 21 | 22 | # Or, using the alias 'On' 23 | on delay "00:05:00" { "Five more minutes!" | Out-Host} 24 | ~~~ 25 | 26 | These example use the built-in event source script 'Delay'. 27 | 28 | Eventful gives you a way to write scripts that will produce events. These are event sources. 29 | 30 | Eventful ships with several event sources. To see what event sources are available to you, run: 31 | 32 | ~~~PowerShell 33 | Get-EventSource # See event sources 34 | ~~~ 35 | 36 | 37 | Each event source discovered when Eventful loads creates a smart alias that makes it easier to discover parameters, for example: 38 | 39 | ~~~PowerShell 40 | # Run in 30 seconds 41 | On@Delay "00:00:30" { "This Message Will Self-Destruct in 30 seconds" | Out-Host } 42 | 43 | 44 | # Run at 5:00 PM 45 | On@Time "5:00 PM" { "End Of Day!" | Out-Host } 46 | 47 | # Run every 2 minutes 48 | On@Repeat "00:02:00" { "Every other minute" | Out-Host } 49 | 50 | # Run whenever a file changes within the current directory 51 | On@FileChange { "Files Changed:$($event.SenderEventArgs)" | Out-Host } 52 | ~~~ 53 | 54 | 55 | Eventful also allows you to handle an arbitrary signal. 56 | In the example below, we set up a handler for "MySignal", and the use the alias send to Send-Event(s). 57 | 58 | ~~~PowerShell 59 | On MySignal {"Fire $($event.MessageData)!" | Out-Host } 60 | 61 | # Send-Event can accept pipeline input for MessageData, and will not output unless -PassThru is specified. 62 | 1..3 | Send MySignal 63 | ~~~ 64 | 65 | --- 66 | 67 | [Understanding Event Sources](Understanding_Event_Sources.md) 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/Export-Event.md: -------------------------------------------------------------------------------- 1 | Export-Event 2 | ------------ 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Exports events 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Exports events to a file for long term storage. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Related Links 25 | * [Import-Event](Import-Event.md) 26 | 27 | 28 | 29 | * [Receive-Event](Receive-Event.md) 30 | 31 | 32 | 33 | 34 | 35 | --- 36 | 37 | 38 | ### Examples 39 | #### EXAMPLE 1 40 | ```PowerShell 41 | Export-Event -OutputPath .\Events.clixml -SourceIdentifier * 42 | ``` 43 | 44 | 45 | 46 | --- 47 | 48 | 49 | ### Parameters 50 | #### **OutputPath** 51 | 52 | The Output Path for the exported events. 53 | 54 | 55 | 56 | 57 | 58 | 59 | |Type |Required|Position|PipelineInput| 60 | |----------|--------|--------|-------------| 61 | |`[String]`|true |1 |false | 62 | 63 | 64 | 65 | #### **SourceIdentifier** 66 | 67 | The source identifier. If not provided, all unhandled events will be exported. 68 | 69 | 70 | 71 | 72 | 73 | 74 | |Type |Required|Position|PipelineInput | 75 | |------------|--------|--------|---------------------| 76 | |`[String[]]`|false |2 |true (ByPropertyName)| 77 | 78 | 79 | 80 | #### **First** 81 | 82 | If provided, will return the first N events 83 | 84 | 85 | 86 | 87 | 88 | 89 | |Type |Required|Position|PipelineInput| 90 | |---------|--------|--------|-------------| 91 | |`[Int32]`|false |3 |false | 92 | 93 | 94 | 95 | #### **Skip** 96 | 97 | If provided, will skip the first N events. 98 | 99 | 100 | 101 | 102 | 103 | 104 | |Type |Required|Position|PipelineInput| 105 | |---------|--------|--------|-------------| 106 | |`[Int32]`|false |4 |false | 107 | 108 | 109 | 110 | #### **Force** 111 | 112 | If set, will overwrite existing files. 113 | If not set, existing files will be read in, and events will be appeneded. 114 | 115 | 116 | 117 | 118 | 119 | 120 | |Type |Required|Position|PipelineInput| 121 | |----------|--------|--------|-------------| 122 | |`[Switch]`|false |named |false | 123 | 124 | 125 | 126 | 127 | 128 | --- 129 | 130 | 131 | ### Syntax 132 | ```PowerShell 133 | Export-Event [-OutputPath] [[-SourceIdentifier] ] [[-First] ] [[-Skip] ] [-Force] [] 134 | ``` 135 | -------------------------------------------------------------------------------- /EventSources/@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 | -------------------------------------------------------------------------------- /docs/HttpResponse-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@HttpResponse.ps1 2 | ------------------------------ 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Sends events on HTTP Responses. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Sends HTTP requests and signals on Responses 18 | 19 | 20 | Event MessageData will contain the response, with two additional properties: 21 | * .ResponseBytes 22 | * .ResponseContent 23 | 24 | 25 | 26 | --- 27 | 28 | 29 | ### Parameters 30 | #### **Uri** 31 | 32 | The Uniform Resource Identifier. 33 | 34 | 35 | 36 | 37 | 38 | 39 | |Type |Required|Position|PipelineInput |Aliases| 40 | |-------|--------|--------|---------------------|-------| 41 | |`[Uri]`|true |1 |true (ByPropertyName)|Url | 42 | 43 | 44 | 45 | #### **Method** 46 | 47 | The HTTP Method 48 | 49 | 50 | 51 | Valid Values: 52 | 53 | * Get 54 | * Head 55 | * Post 56 | * Put 57 | * Delete 58 | * Trace 59 | * Options 60 | * Merge 61 | * Patch 62 | 63 | 64 | 65 | 66 | 67 | 68 | |Type |Required|Position|PipelineInput | 69 | |----------|--------|--------|---------------------| 70 | |`[String]`|false |2 |true (ByPropertyName)| 71 | 72 | 73 | 74 | #### **Header** 75 | 76 | A collection of headers to send with the request. 77 | 78 | 79 | 80 | 81 | 82 | 83 | |Type |Required|Position|PipelineInput |Aliases| 84 | |---------------|--------|--------|---------------------|-------| 85 | |`[IDictionary]`|false |3 |true (ByPropertyName)|Headers| 86 | 87 | 88 | 89 | #### **Body** 90 | 91 | The request body. 92 | 93 | 94 | 95 | 96 | 97 | 98 | |Type |Required|Position|PipelineInput | 99 | |------------|--------|--------|---------------------| 100 | |`[PSObject]`|false |4 |true (ByPropertyName)| 101 | 102 | 103 | 104 | #### **PollingInterval** 105 | 106 | The polling interval. 107 | This is the minimum amount of time until you will be notified of the success or failure of a http request 108 | 109 | 110 | 111 | 112 | 113 | 114 | |Type |Required|Position|PipelineInput | 115 | |------------|--------|--------|---------------------| 116 | |`[TimeSpan]`|false |4 |true (ByPropertyName)| 117 | 118 | 119 | 120 | #### **TransferEncoding** 121 | 122 | 123 | 124 | 125 | |Type |Required|Position|PipelineInput| 126 | |------------|--------|--------|-------------| 127 | |`[Encoding]`|false |named |false | 128 | 129 | 130 | 131 | 132 | 133 | --- 134 | 135 | 136 | ### Syntax 137 | ```PowerShell 138 | EventSources/@HttpResponse.ps1 [-Uri] [[-Method] ] [[-Header] ] [[-Body] ] [[-PollingInterval] ] [-TransferEncoding ] [] 139 | ``` 140 | -------------------------------------------------------------------------------- /docs/_posts/2023-06-10-Eventful-0.1.8.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | title: Eventful 0.1.8 4 | sourceURL: https://github.com/StartAutomating/Eventful/releases/tag/v0.1.8 5 | tag: release 6 | --- 7 | ## Eventful 0.1.8: 8 | 9 | * Eventful Supports Sponsorship ([#25](https://github.com/StartAutomating/Eventful/issues/25)) 10 | * New Commands! 11 | * Import-Event ([#28](https://github.com/StartAutomating/Eventful/issues/28)) 12 | * Export-Event ([#27](https://github.com/StartAutomating/Eventful/issues/27)) 13 | * Send-Event can now send -EventArguments and -MessageData ([#26](https://github.com/StartAutomating/Eventful/issues/26)) 14 | * Watch-Event now supports -MaxTriggerCount and -MessageData ([#29](https://github.com/StartAutomating/Eventful/issues/29)) 15 | * Simplifying event source registration (any `@*` script or function) ([#30](https://github.com/StartAutomating/Eventful/issues/30)) 16 | * Making one-time event sources more efficient ([#31](https://github.com/StartAutomating/Eventful/issues/31)) 17 | 18 | --- 19 | 20 | ## Eventful 0.1.7: 21 | * Adding On@CommandNotFound event source (Fixes [#11](https://github.com/StartAutomating/Eventful/issues/11)) 22 | * Watch-Event now allows eventsources -recursively (Fixes [#15](https://github.com/StartAutomating/Eventful/issues/15)) 23 | 24 | --- 25 | 26 | ## Eventful 0.1.6 27 | * Adding LocationChanged event source (Fixes [#12](https://github.com/StartAutomating/Eventful/issues/12)) 28 | 29 | --- 30 | 31 | ## Eventful 0.1.5 32 | * Adding On@Event ([#2](https://github.com/StartAutomating/Eventful/issues/2)) 33 | * Send-Event support for piping existing events ([#4](https://github.com/StartAutomating/Eventful/issues/4)) 34 | * Adding /docs ([#5](https://github.com/StartAutomating/Eventful/issues/5)) 35 | 36 | --- 37 | 38 | ## Eventful 0.1.4 39 | 40 | * Module Rebranded to Eventful. 41 | * Get-EventHandler added 42 | 43 | --- 44 | 45 | ## Eventful 0.1.3 46 | New Event Source: 47 | * VariableSet 48 | 49 | Receive-Event now returns event most-recent to least-recent. 50 | Receive-Event now has -First and -Skip. 51 | 52 | Bugfix: On@Repeat now actually starts it's timer. 53 | 54 | --- 55 | 56 | ## Eventful 0.1.2 57 | New Event Source: 58 | * UDP 59 | 60 | PowerShellAsync Event Source now allows for a -Parameter dictionaries. 61 | 62 | --- 63 | 64 | ## Eventful 0.1.1 65 | New Event Sources: 66 | * HTTPResponse 67 | * PowerShellAsync 68 | 69 | --- 70 | 71 | New Event Source Capabilities: 72 | 73 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute. 74 | This will be called directly after the subscription is created, so as to avoid signalling too soon. 75 | 76 | --- 77 | 78 | ## Eventful 0.1 79 | 80 | Initial Module Release. 81 | 82 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"}) 83 | Better pipelining support for Sending events. 84 | 85 | --- 86 | -------------------------------------------------------------------------------- /Eventful.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'Eventful.psm1' 3 | Description = 'Easy Eventful PowerShell' 4 | ModuleVersion = '0.1.8' 5 | GUID = 'f4d780da-be78-49c6-921a-436e053cb97c' 6 | Author = 'James Brundage' 7 | Copyright = '2021-2023 Start-Automating' 8 | FormatsToProcess = 'Eventful.format.ps1xml' 9 | TypesToProcess = 'Eventful.types.ps1xml' 10 | AliasesToExport = '*' 11 | PrivateData = @{ 12 | PSData = @{ 13 | ProjectURI = 'https://github.com/StartAutomating/Eventful' 14 | LicenseURI = 'https://github.com/StartAutomating/Eventful/blob/main/LICENSE' 15 | 16 | Tags = 'Eventful', 'Events' 17 | 18 | ReleaseNotes = @' 19 | ## Eventful 0.1.8: 20 | 21 | * Eventful Supports Sponsorship (#25) 22 | * New Commands! 23 | * Import-Event (#28) 24 | * Export-Event (#27) 25 | * Send-Event can now send -EventArguments and -MessageData (#26) 26 | * Watch-Event now supports -MaxTriggerCount and -MessageData (#29) 27 | * Simplifying event source registration (any `@*` script or function) (#30) 28 | * Making one-time event sources more efficient (#31) 29 | 30 | --- 31 | 32 | ## Eventful 0.1.7: 33 | * Adding On@CommandNotFound event source (Fixes #11) 34 | * Watch-Event now allows eventsources -recursively (Fixes #15) 35 | 36 | --- 37 | 38 | ## Eventful 0.1.6 39 | * Adding LocationChanged event source (Fixes #12) 40 | 41 | --- 42 | 43 | ## Eventful 0.1.5 44 | * Adding On@Event (#2) 45 | * Send-Event support for piping existing events (#4) 46 | * Adding /docs (#5) 47 | 48 | --- 49 | 50 | ## Eventful 0.1.4 51 | 52 | * Module Rebranded to Eventful. 53 | * Get-EventHandler added 54 | 55 | --- 56 | 57 | ## Eventful 0.1.3 58 | New Event Source: 59 | * VariableSet 60 | 61 | Receive-Event now returns event most-recent to least-recent. 62 | Receive-Event now has -First and -Skip. 63 | 64 | Bugfix: On@Repeat now actually starts it's timer. 65 | 66 | --- 67 | 68 | ## Eventful 0.1.2 69 | New Event Source: 70 | * UDP 71 | 72 | PowerShellAsync Event Source now allows for a -Parameter dictionaries. 73 | 74 | --- 75 | 76 | ## Eventful 0.1.1 77 | New Event Sources: 78 | * HTTPResponse 79 | * PowerShellAsync 80 | 81 | --- 82 | 83 | New Event Source Capabilities: 84 | 85 | Event Sources can now return an InitializeEvent property or provide a ComponentModel.InitializationEvent attribute. 86 | This will be called directly after the subscription is created, so as to avoid signalling too soon. 87 | 88 | --- 89 | 90 | ## Eventful 0.1 91 | 92 | Initial Module Release. 93 | 94 | Fun simple event syntax (e.g. on mysignal {"do this"} or on delay "00:00:01" {"do that"}) 95 | Better pipelining support for Sending events. 96 | 97 | --- 98 | '@ 99 | } 100 | 101 | CommandTypes = @{ 102 | EventSource = '^\@\w' 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /docs/FileChange-EventSource.md: -------------------------------------------------------------------------------- 1 | EventSources/@FileChange.ps1 2 | ---------------------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Watches for File Changes. 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Uses the [IO.FileSystemWatcher] to watch for changes to files. 18 | 19 | Because some applications and frameworks write to files differently, 20 | you may see more than one event for a given change. 21 | 22 | 23 | 24 | --- 25 | 26 | 27 | ### Parameters 28 | #### **FilePath** 29 | 30 | The path to the file or directory 31 | 32 | 33 | 34 | 35 | 36 | 37 | |Type |Required|Position|PipelineInput |Aliases | 38 | |----------|--------|--------|---------------------|--------| 39 | |`[String]`|false |1 |true (ByPropertyName)|Fullname| 40 | 41 | 42 | 43 | #### **FileFilter** 44 | 45 | A wildcard filter describing the names of files to watch 46 | 47 | 48 | 49 | 50 | 51 | 52 | |Type |Required|Position|PipelineInput | 53 | |----------|--------|--------|---------------------| 54 | |`[String]`|false |2 |true (ByPropertyName)| 55 | 56 | 57 | 58 | #### **NotifyFilter** 59 | 60 | A notify filter describing the file changes that should raise events. 61 | 62 | 63 | 64 | Valid Values: 65 | 66 | * FileName 67 | * DirectoryName 68 | * Attributes 69 | * Size 70 | * LastWrite 71 | * LastAccess 72 | * CreationTime 73 | * Security 74 | 75 | 76 | 77 | 78 | 79 | 80 | |Type |Required|Position|PipelineInput | 81 | |-------------------|--------|--------|---------------------| 82 | |`[NotifyFilters[]]`|false |3 |true (ByPropertyName)| 83 | 84 | 85 | 86 | #### **Recurse** 87 | 88 | If set, will include subdirectories in the watcher. 89 | 90 | 91 | 92 | 93 | 94 | 95 | |Type |Required|Position|PipelineInput|Aliases | 96 | |----------|--------|--------|-------------|---------------------------------------------| 97 | |`[Switch]`|false |named |false |InludeSubsdirectory
InludeSubsdirectories| 98 | 99 | 100 | 101 | #### **EventName** 102 | 103 | The names of the file change events to watch. 104 | By default, watches for Changed, Created, Deleted, or Renamed 105 | 106 | 107 | 108 | Valid Values: 109 | 110 | * Changed 111 | * Created 112 | * Deleted 113 | * Renamed 114 | 115 | 116 | 117 | 118 | 119 | 120 | |Type |Required|Position|PipelineInput| 121 | |------------|--------|--------|-------------| 122 | |`[String[]]`|false |4 |false | 123 | 124 | 125 | 126 | 127 | 128 | --- 129 | 130 | 131 | ### Syntax 132 | ```PowerShell 133 | EventSources/@FileChange.ps1 [[-FilePath] ] [[-FileFilter] ] [[-NotifyFilter] {FileName | DirectoryName | Attributes | Size | LastWrite | LastAccess | CreationTime | Security}] [-Recurse] [[-EventName] ] [] 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/Get-EventSource.md: -------------------------------------------------------------------------------- 1 | Get-EventSource 2 | --------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Gets Event Sources 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Gets Event Sources. 18 | 19 | Event sources are commands or script blocks that can generate events. 20 | 21 | Event sources can be implemented in: 22 | * A .PS1 file starting with @ 23 | * An in-memory scriptblock variable starting with @ 24 | * A module command referenced within a PrivateData.OnQ section of the module manifest. 25 | 26 | 27 | 28 | --- 29 | 30 | 31 | ### Related Links 32 | * [Watch-Event](Watch-Event.md) 33 | 34 | 35 | 36 | 37 | 38 | --- 39 | 40 | 41 | ### Examples 42 | #### EXAMPLE 1 43 | ```PowerShell 44 | Get-EventSource 45 | ``` 46 | 47 | #### EXAMPLE 2 48 | ```PowerShell 49 | Get-EventSource -Subscription 50 | ``` 51 | 52 | 53 | 54 | --- 55 | 56 | 57 | ### Parameters 58 | #### **Name** 59 | 60 | The name of the event source. 61 | 62 | 63 | 64 | 65 | 66 | 67 | |Type |Required|Position|PipelineInput | 68 | |------------|--------|--------|---------------------| 69 | |`[String[]]`|false |1 |true (ByPropertyName)| 70 | 71 | 72 | 73 | #### **Subscription** 74 | 75 | If set, will get subscriptions related to event sources. 76 | 77 | 78 | 79 | 80 | 81 | 82 | |Type |Required|Position|PipelineInput | 83 | |----------|--------|--------|---------------------| 84 | |`[Switch]`|false |named |true (ByPropertyName)| 85 | 86 | 87 | 88 | #### **SourceObject** 89 | 90 | If set, will get source objects from the subscriptions related to event sources. 91 | 92 | 93 | 94 | 95 | 96 | 97 | |Type |Required|Position|PipelineInput | 98 | |----------|--------|--------|---------------------| 99 | |`[Switch]`|false |named |true (ByPropertyName)| 100 | 101 | 102 | 103 | #### **Help** 104 | 105 | If set, will get full help for each event source. 106 | 107 | 108 | 109 | 110 | 111 | 112 | |Type |Required|Position|PipelineInput | 113 | |----------|--------|--------|---------------------| 114 | |`[Switch]`|false |named |true (ByPropertyName)| 115 | 116 | 117 | 118 | 119 | 120 | --- 121 | 122 | 123 | ### Outputs 124 | * Eventful.EventSource 125 | 126 | 127 | * [Management.Automation.CommandInfo](https://learn.microsoft.com/en-us/dotnet/api/System.Management.Automation.CommandInfo) 128 | 129 | 130 | * [Management.Automation.PSEventSubscriber](https://learn.microsoft.com/en-us/dotnet/api/System.Management.Automation.PSEventSubscriber) 131 | 132 | 133 | * [Management.Automation.PSObject](https://learn.microsoft.com/en-us/dotnet/api/System.Management.Automation.PSObject) 134 | 135 | 136 | 137 | 138 | 139 | 140 | --- 141 | 142 | 143 | ### Syntax 144 | ```PowerShell 145 | Get-EventSource [[-Name] ] [-Subscription] [-SourceObject] [-Help] [] 146 | ``` 147 | -------------------------------------------------------------------------------- /docs/Send-Event.md: -------------------------------------------------------------------------------- 1 | Send-Event 2 | ---------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Sends Events 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Sends Events to PowerShell. 18 | 19 | Send-Event is a wrapper for the built-in command New-Event with a few key differences: 20 | 1. It allows MessageData to be piped in 21 | 2. You can send multiple sourceidentifiers 22 | 3. It does not output by default (you must pass -PassThru) 23 | 24 | 25 | 26 | --- 27 | 28 | 29 | ### Related Links 30 | * [New-Event](https://docs.microsoft.com/powershell/module/Microsoft.PowerShell.Utility/New-Event) 31 | 32 | 33 | 34 | * [Watch-Event](Watch-Event.md) 35 | 36 | 37 | 38 | 39 | 40 | --- 41 | 42 | 43 | ### Examples 44 | #### EXAMPLE 1 45 | ```PowerShell 46 | 1..4 | Send-Event "Hit It" 47 | ``` 48 | 49 | 50 | 51 | --- 52 | 53 | 54 | ### Parameters 55 | #### **SourceIdentifier** 56 | 57 | The SourceIdentifier 58 | 59 | 60 | 61 | 62 | 63 | 64 | |Type |Required|Position|PipelineInput | 65 | |------------|--------|--------|---------------------| 66 | |`[String[]]`|true |1 |true (ByPropertyName)| 67 | 68 | 69 | 70 | #### **MessageData** 71 | 72 | The message data 73 | 74 | 75 | 76 | 77 | 78 | 79 | |Type |Required|Position|PipelineInput | 80 | |------------|--------|--------|------------------------------| 81 | |`[PSObject]`|false |2 |true (ByValue, ByPropertyName)| 82 | 83 | 84 | 85 | #### **Sender** 86 | 87 | The sender. 88 | 89 | 90 | 91 | 92 | 93 | 94 | |Type |Required|Position|PipelineInput | 95 | |------------|--------|--------|---------------------| 96 | |`[PSObject]`|false |3 |true (ByPropertyName)| 97 | 98 | 99 | 100 | #### **EventArguments** 101 | 102 | The event arguments. 103 | 104 | 105 | 106 | 107 | 108 | 109 | |Type |Required|Position|PipelineInput |Aliases | 110 | |------------|--------|--------|---------------------|-----------------------------| 111 | |`[PSObject]`|false |4 |true (ByPropertyName)|SourceEventArgs
EventArgs| 112 | 113 | 114 | 115 | #### **PassThru** 116 | 117 | If set, will output the created event. 118 | 119 | 120 | 121 | 122 | 123 | 124 | |Type |Required|Position|PipelineInput | 125 | |----------|--------|--------|---------------------| 126 | |`[Switch]`|false |named |true (ByPropertyName)| 127 | 128 | 129 | 130 | 131 | 132 | --- 133 | 134 | 135 | ### Outputs 136 | * [Nullable](https://learn.microsoft.com/en-us/dotnet/api/System.Nullable) 137 | 138 | 139 | * [Management.Automation.PSEventArgs](https://learn.microsoft.com/en-us/dotnet/api/System.Management.Automation.PSEventArgs) 140 | 141 | 142 | 143 | 144 | 145 | 146 | --- 147 | 148 | 149 | ### Syntax 150 | ```PowerShell 151 | Send-Event [-SourceIdentifier] [[-MessageData] ] [[-Sender] ] [[-EventArguments] ] [-PassThru] [] 152 | ``` 153 | -------------------------------------------------------------------------------- /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,ValueFromPipelineByPropertyName)] 24 | [string[]] 25 | $SourceIdentifier, 26 | 27 | # The message data 28 | [Parameter(ValueFromPipeline,Position=1,ValueFromPipelineByPropertyName)] 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 | [Alias('SourceEventArgs','EventArgs')] 40 | [PSObject] 41 | $EventArguments, 42 | 43 | # If set, will output the created event. 44 | [Parameter(ValueFromPipelineByPropertyName)] 45 | [switch] 46 | $PassThru 47 | ) 48 | 49 | 50 | begin { 51 | # Be we start, get a reference to New-Event 52 | $newEvent = $ExecutionContext.SessionState.InvokeCommand.GetCommand('New-Event','Cmdlet') 53 | } 54 | process { 55 | #region Map New-Event Parameters 56 | $myParameters = @{} + $PSBoundParameters 57 | $newEventParams = @{} + $PSBoundParameters # Copy bound parameters. 58 | foreach ($k in @($newEventParams.Keys)) { 59 | if (-not $newEvent.Parameters[$k]) { # If a parameter isn't for New-Event 60 | $newEventParams.Remove($k) # remove it from the copy. 61 | } 62 | } 63 | # If we're piping in MessageData, but the message only contains "Sender" and "Event" 64 | if ($newEventParams.Sender -and $newEventParams.EventArgs -and 65 | $newEventParams.MessageData.psobject.properties.Count -eq 2 -and 66 | $newEventParams.MessageData.Sender -and $newEventParams.MessageData.EventArgs) { 67 | $newEventParams.Remove('MessageData') # remove the MessageData. 68 | } 69 | # Always remove the sourceID parameter (New-Event allows one, Send-Event allows many) 70 | $newEventParams.Remove('SourceIdentifier') 71 | #endregion Map New-Event Parameters 72 | 73 | #region Send Each Event 74 | foreach ($sourceID in $SourceIdentifier) { # Walk over each source identifier 75 | # and call New-Event. 76 | $evt = New-Event @newEventParams -SourceIdentifier $sourceID 77 | if ($PassThru) { # If we want to -PassThru events 78 | $evt # output the created event. 79 | } 80 | } 81 | #endregion Send Each Event 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /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 | Add-Member MaxTriggerCount 1 -Force -PassThru 88 | 89 | return 90 | } -------------------------------------------------------------------------------- /Export-Event.ps1: -------------------------------------------------------------------------------- 1 | function Export-Event 2 | { 3 | <# 4 | .Synopsis 5 | Exports events 6 | .Description 7 | Exports events to a file for long term storage. 8 | .Example 9 | Export-Event -OutputPath .\Events.clixml -SourceIdentifier * 10 | .LINK 11 | Import-Event 12 | .Link 13 | Receive-Event 14 | #> 15 | param( 16 | # The Output Path for the exported events. 17 | [ValidateScript({ 18 | $extension = @($_ -split '\.')[-1] 19 | $exporter = $ExecutionContext.SessionState.InvokeCommand.GetCommand( 20 | "Export-$Extension", 21 | "Function,Alias,Cmdlet" 22 | ) 23 | if (-not $exporter) { 24 | throw "Export-$Extension does not exist. Cannot export .$Extension" 25 | } 26 | return $true 27 | })] 28 | [Parameter(Mandatory)] 29 | [string] 30 | $OutputPath, 31 | 32 | # The source identifier. If not provided, all unhandled events will be exported. 33 | [Parameter(ValueFromPipelineByPropertyName)] 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 | # If set, will overwrite existing files. 46 | # If not set, existing files will be read in, and events will be appeneded. 47 | [switch] 48 | $Force 49 | ) 50 | 51 | begin { 52 | $accumulatedEvents = [Collections.Queue]::new() 53 | } 54 | process { 55 | if ($_ -is [Management.Automation.PSEvent]) { 56 | $accumulatedEvents.Enqueue($_) 57 | return 58 | } 59 | 60 | $receiveSplat = @{} + $PSBoundParameters 61 | $receiveSplat.Remove('OutputPath') 62 | $receiveSplat.Remove('Force') 63 | if (-not $receiveSplat.SourceIdentifier) { 64 | $receiveSplat.SourceIdentifier = '*' 65 | } 66 | foreach ($eventInfo in Receive-Event @receiveSplat) { 67 | $accumulatedEvents.Enqueue($eventInfo) 68 | } 69 | 70 | if (-not $accumulatedEvents.Count) { 71 | Write-Verbose "No Events Received. Nothing to export." 72 | return 73 | } 74 | } 75 | 76 | 77 | end { 78 | $outputPathExists = Test-Path $OutputPath 79 | 80 | # try to find an exporter 81 | $exporter = $ExecutionContext.SessionState.InvokeCommand.GetCommand( 82 | # (by looking for any Export-Command that shares a name with the extension) 83 | "Export-$(@($OutputPath -split '\.')[-1])", 84 | 'Function,Alias,Cmdlet' 85 | ) 86 | 87 | if (-not $exporter) { 88 | Write-Error "No Exporter found for $OutputPath" 89 | return 90 | } 91 | 92 | if ($outputPathExists -and -not $force) { 93 | $importer = $ExecutionContext.SessionState.InvokeCommand.GetCommand( 94 | # (by looking for any Import-Command that shares a name with the extension) 95 | "Import-$(@($OutputPath -split '\.')[-1])", 96 | 'Function,Alias,Cmdlet' 97 | ) 98 | $receivedEvents = @() + @(& $importer $OutputPath) + $accumulatedEvents.ToArray() 99 | } else { 100 | $receivedEvents = $accumulatedEvents.ToArray() 101 | } 102 | 103 | $receivedEvents | & $exporter $OutputPath 104 | 105 | if ($?) { 106 | Get-Item $OutputPath 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docs/Receive-Event.md: -------------------------------------------------------------------------------- 1 | Receive-Event 2 | ------------- 3 | 4 | 5 | 6 | 7 | ### Synopsis 8 | Receives Events 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | ### Description 16 | 17 | Receives Events and output from Event Subscriptions. 18 | 19 | 20 | 21 | --- 22 | 23 | 24 | ### Related Links 25 | * [Send-Event](Send-Event.md) 26 | 27 | 28 | 29 | * [Watch-Event](Watch-Event.md) 30 | 31 | 32 | 33 | 34 | 35 | --- 36 | 37 | 38 | ### Examples 39 | #### EXAMPLE 1 40 | ```PowerShell 41 | Get-EventSource -Subscriber | Receive-Event 42 | ``` 43 | 44 | #### EXAMPLE 2 45 | ```PowerShell 46 | Receive-Event -SourceIdentifier * -First 1 # Receives the most recent event with any source identifier. 47 | ``` 48 | 49 | 50 | 51 | --- 52 | 53 | 54 | ### Parameters 55 | #### **SubscriptionID** 56 | 57 | The event subscription ID. 58 | 59 | 60 | 61 | 62 | 63 | 64 | |Type |Required|Position|PipelineInput | 65 | |-----------|--------|--------|---------------------| 66 | |`[Int32[]]`|true |named |true (ByPropertyName)| 67 | 68 | 69 | 70 | #### **EventIdentifier** 71 | 72 | The event ID. 73 | 74 | 75 | 76 | 77 | 78 | 79 | |Type |Required|Position|PipelineInput | 80 | |-----------|--------|--------|---------------------| 81 | |`[Int32[]]`|true |named |true (ByPropertyName)| 82 | 83 | 84 | 85 | #### **SourceIdentifier** 86 | 87 | The event source identifier. 88 | 89 | 90 | 91 | 92 | 93 | 94 | |Type |Required|Position|PipelineInput | 95 | |------------|--------|--------|---------------------| 96 | |`[String[]]`|false |named |true (ByPropertyName)| 97 | 98 | 99 | 100 | #### **First** 101 | 102 | If provided, will return the first N events 103 | 104 | 105 | 106 | 107 | 108 | 109 | |Type |Required|Position|PipelineInput| 110 | |---------|--------|--------|-------------| 111 | |`[Int32]`|false |named |false | 112 | 113 | 114 | 115 | #### **Skip** 116 | 117 | If provided, will skip the first N events. 118 | 119 | 120 | 121 | 122 | 123 | 124 | |Type |Required|Position|PipelineInput| 125 | |---------|--------|--------|-------------| 126 | |`[Int32]`|false |named |false | 127 | 128 | 129 | 130 | #### **InputObject** 131 | 132 | The input object. 133 | If the Input Object was a job, it will receive the results of the job. 134 | 135 | 136 | 137 | 138 | 139 | 140 | |Type |Required|Position|PipelineInput | 141 | |------------|--------|--------|--------------| 142 | |`[PSObject]`|false |named |true (ByValue)| 143 | 144 | 145 | 146 | #### **Clear** 147 | 148 | If set, will remove events from the system after they have been returned, 149 | and will not keep results from Jobs or Event Handlers. 150 | 151 | 152 | 153 | 154 | 155 | 156 | |Type |Required|Position|PipelineInput| 157 | |----------|--------|--------|-------------| 158 | |`[Switch]`|false |named |false | 159 | 160 | 161 | 162 | 163 | 164 | --- 165 | 166 | 167 | ### Outputs 168 | * [Management.Automation.PSObject](https://learn.microsoft.com/en-us/dotnet/api/System.Management.Automation.PSObject) 169 | 170 | 171 | * [Management.Automation.PSEventArgs](https://learn.microsoft.com/en-us/dotnet/api/System.Management.Automation.PSEventArgs) 172 | 173 | 174 | 175 | 176 | 177 | 178 | --- 179 | 180 | 181 | ### Syntax 182 | ```PowerShell 183 | Receive-Event [-First ] [-Skip ] [-InputObject ] [-Clear] [] 184 | ``` 185 | ```PowerShell 186 | Receive-Event -SubscriptionID [-SourceIdentifier ] [-First ] [-Skip ] [-InputObject ] [-Clear] [] 187 | ``` 188 | ```PowerShell 189 | Receive-Event -EventIdentifier [-SourceIdentifier ] [-First ] [-Skip ] [-InputObject ] [-Clear] [] 190 | ``` 191 | ```PowerShell 192 | Receive-Event -SourceIdentifier [-First ] [-Skip ] [-InputObject ] [-Clear] [] 193 | ``` 194 | -------------------------------------------------------------------------------- /Eventful.types.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Eventful.EventHandler 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 | Help 23 | 24 | Get-Help $this.EventSourceId 25 | 26 | 27 | 28 | 29 | IsSpecificEvent 30 | 31 | if ($this.Name -match '\.(events|handlers).ps1$') { 32 | $false 33 | } else { 34 | $true 35 | } 36 | 37 | 38 | 39 | 40 | 41 | Name 42 | 43 | $realName = $this.GetType().GetProperty("Name").GetGetMethod().Invoke($this, 'Instance,Public', $null, $null, $null) 44 | if ($realName -match '\.(events|handlers).ps1$') { 45 | $realName -replace '\.(events|handlers).ps1$' 46 | } else { 47 | $realName -replace '^On_' -replace '\.handler' -replace '\.ps1$' 48 | } 49 | 50 | 51 | 52 | 53 | 54 | SourceIdentifier 55 | 56 | if ($this.Name -match '\.(events|handlers).ps1$') { 57 | '' 58 | } else { 59 | $this.Name -replace '^On_' -replace '\.handler' -replace '\.ps1$' 60 | } 61 | 62 | 63 | 64 | 65 | Synopsis 66 | 67 | # From ?<PowerShell_HelpField> in Irregular (https://github.com/StartAutomating/Irregular) 68 | [Regex]::new(@' 69 | \.(?<Field>Synopsis) # Field Start 70 | \s{0,} # Optional Whitespace 71 | (?<Content>(.|\s)+?(?=(\.\w+|\#\>))) # Anything until the next .\field or end of the comment block 72 | '@, 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Match( 73 | $this.ScriptBlock 74 | ).Groups["Content"].Value 75 | 76 | 77 | 78 | 79 | 80 | 81 | Eventful.EventSource 82 | 83 | 84 | Description 85 | 86 | # From ?<PowerShell_HelpField> in Irregular (https://github.com/StartAutomating/Irregular) 87 | [Regex]::new(@' 88 | \.(?<Field>Description) # Field Start 89 | \s{0,} # Optional Whitespace 90 | (?<Content>(.|\s)+?(?=(\.\w+|\#\>))) # Anything until the next .\field or end of the comment block 91 | '@, 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Match( 92 | $this.ScriptBlock 93 | ).Groups["Content"].Value 94 | 95 | 96 | 97 | 98 | EventSourceID 99 | 100 | if ($this -is [Management.Automation.ExternalScriptInfo]) { 101 | $this.Path # the key is the path. 102 | } elseif ($this.Module) { # If it was from a module 103 | $this.Module + '\' + $this.Name # it's the module qualified name. 104 | } else { 105 | $this.Name # Otherwise, it's just the function name. 106 | } 107 | 108 | 109 | 110 | 111 | Help 112 | 113 | Get-Help $this.EventSourceId 114 | 115 | 116 | 117 | 118 | Synopsis 119 | 120 | # From ?<PowerShell_HelpField> in Irregular (https://github.com/StartAutomating/Irregular) 121 | [Regex]::new(@' 122 | \.(?<Field>Synopsis) # Field Start 123 | \s{0,} # Optional Whitespace 124 | (?<Content>(.|\s)+?(?=(\.\w+|\#\>))) # Anything until the next .\field or end of the comment block 125 | '@, 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Match( 126 | $this.ScriptBlock 127 | ).Groups["Content"].Value 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /Eventful.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 Eventful { 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 750; "done" } 59 | 60 | $j| 61 | On@Job -Then { $global:JobsIsDone = $true } 62 | 63 | do { 64 | Start-Sleep -Milliseconds 1000 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@repeat -interval "00:00:00.1" -Then {1} # Signal every tenth of a second. 89 | Start-Sleep -Milliseconds 250 90 | $receivedResults = @(Get-EventSource -Name Repeat -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 "* 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 | -------------------------------------------------------------------------------- /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 Ignore 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 | 70 | $HttpResponseChecker = 71 | Register-ObjectEvent -InputObject $httpResponseCheckTimer -EventName Elapsed -Action { 72 | $toCallEnd = # check to see if any requests have completed. 73 | @(foreach ($httpAsyncInfo in $Global:HttpResponsesAsync) { 74 | if ($httpAsyncInfo.IAsyncResult.IsCompleted) { 75 | $httpAsyncInfo 76 | } 77 | }) 78 | 79 | $null = # Foreach completed request 80 | foreach ($httpAsyncInfo in $toCallEnd) { 81 | $webResponse = 82 | try { # try to get the response 83 | $httpAsyncInfo.InputObject.EndGetResponse($httpAsyncInfo.IAsyncResult) 84 | } catch { 85 | $_ # and catch any error. 86 | } 87 | 88 | 89 | if ($webResponse -is [Management.Automation.ErrorRecord] -or 90 | $webResponse -is [Exception]) # If we got an error 91 | { 92 | # Signal the error 93 | New-Event -SourceIdentifier "HttpRequest.Failed.$($httpAsyncInfo.InvokeID)" -MessageData $webResponse 94 | $Global:HttpResponsesAsync.Remove($httpAsyncInfo) 95 | continue 96 | } 97 | 98 | 99 | # Otherwise, get the response stream 100 | $ms = [IO.MemoryStream]::new() 101 | $null = $webResponse.GetResponseStream().CopyTo($ms) 102 | $responseBytes = $ms.ToArray() # as a [byte[]]. 103 | $ms.Close() 104 | $ms.Dispose() 105 | 106 | 107 | $encoding = # See if the response had an encoding 108 | if ($webResponse.ContentEncoding) { $webResponse.ContentEncoding } 109 | elseif ($webResponse.CharacterSet) { 110 | # or a character set. 111 | [Text.Encoding]::GetEncodings() | Where-Object Name -EQ $webResponse.CharacterSet 112 | } 113 | 114 | $webResponseContent = 115 | if ($encoding) { # If it did, decode the response content. 116 | [IO.StreamReader]::new([IO.MemoryStream]::new($responseBytes), $encoding).ReadToEnd() 117 | } else { 118 | $null 119 | } 120 | 121 | # Add the properties to the web response. 122 | $webResponse | 123 | Add-Member NoteProperty ResponseBytes $webResponseContent -Force -PassThru | 124 | Add-Member NoteProperty ResponseContent $webResponseContent -Force 125 | $webResponse.Close() 126 | 127 | # And send the response with the additional information. 128 | New-Event -SourceIdentifier "HttpRequest.Completed.$($httpAsyncInfo.InvokeID)" -MessageData $webResponse 129 | $Global:HttpResponsesAsync.Remove($httpAsyncInfo) 130 | } 131 | 132 | if ($Global:HttpResponsesAsync.Count -eq 0) { 133 | Get-EventSubscriber -SourceIdentifier "@HttpResponse_Check" | Unregister-Event 134 | } 135 | } 136 | 137 | $httpResponseCheckTimer.Start() # Start it's timer. 138 | 139 | 140 | $httpRequest = [Net.HttpWebRequest]::CreateHttp($Uri) 141 | $httpRequest.Method = $Method 142 | if ($Header -and $Header.Count) { 143 | foreach ($kv in $Header.GetEnumerator()) { 144 | $httpRequest.Headers[$kv.Key] = $kv.Value 145 | } 146 | } 147 | if ($Body) { 148 | $requestStream = $httpRequest.GetRequestStream() 149 | 150 | if (-not $requestStream) { return } 151 | if ($body -is [byte[]] -or $Body -as [byte[]]) { 152 | [IO.MemoryStream]::new([byte[]]$Body).CopyTo($requestStream) 153 | } 154 | elseif ($Body -is [string]) { 155 | [IO.StreamWriter]::new($requestStream, $TransferEncoding).Write($Body) 156 | } 157 | else { 158 | [IO.StreamWriter]::new($requestStream, $TransferEncoding).Write((ConvertTo-Json -InputObject $body -Depth 100)) 159 | } 160 | } 161 | $requestId = [Guid]::NewGuid().ToString() 162 | $httpRequest | 163 | Add-Member NoteProperty SourceIdentifier "HttpRequest.Completed.$requestId","HttpRequest.Failed.$requestId" -Force -PassThru | 164 | Add-Member NoteProperty RequestID $requestId -Force -PassThru 165 | } -------------------------------------------------------------------------------- /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('Eventful.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) -match '^@\w' 50 | 51 | # Save a pointer to the method for terseness and speed. 52 | $getCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand 53 | 54 | $myInv = $MyInvocation 55 | 56 | $lookInDirectory = @( 57 | "$pwd" 58 | $myRoot = $myInv.MyCommand.ScriptBlock.File | Split-Path -ErrorAction SilentlyContinue 59 | "$myRoot" 60 | if ($myInv.MyCommand.Module) { # Assuming, of course, we have a module. 61 | $MyModuleRoot = $myInv.MyCommand.Module | Split-Path -ErrorAction SilentlyContinue 62 | if ($MyModuleRoot -ne $myRoot) { "$MyModuleRoot" } 63 | } 64 | ) | Select-Object -Unique 65 | 66 | $atScripts = $lookInDirectory | 67 | Get-Item | 68 | Get-ChildItem -Filter '@*.ps1' -Recurse | 69 | & { process { 70 | if ($_.Name -notmatch '^\@\w') { return } 71 | $getCmd.Invoke($_.Fullname,'ExternalScript') 72 | } } 73 | 74 | # If we had a module, and we still don't have a match, we'll look for extensions. 75 | 76 | $loadedModules = @(Get-Module) 77 | 78 | if ($loadedModules -notcontains $myInv.MyCommand.Module) { 79 | $loadedModules = @($myInv.MyCommand.Module) + $loadedModules 80 | } 81 | $myModuleName = $myInv.MyCommand.Module.Name 82 | $extendedCommands = 83 | 84 | foreach ($loadedModule in $loadedModules) { # Walk over all modules. 85 | if ( # If the module has PrivateData keyed to this module 86 | $loadedModule.PrivateData.$myModuleName 87 | ) { 88 | # Determine the root of the module with private data. 89 | $thisModuleRoot = [IO.Path]::GetDirectoryName($loadedModule.Path) 90 | # and get the extension data 91 | $extensionData = $loadedModule.PrivateData.$myModuleName 92 | if ($extensionData -is [Hashtable]) { # If it was a hashtable 93 | foreach ($ed in $extensionData.GetEnumerator()) { # walk each key 94 | 95 | $extensionCmd = 96 | if ($ed.Value -like '*.ps1') { # If the key was a .ps1 file 97 | $getCmd.Invoke( # treat it as a relative path to the .ps1 98 | [IO.Path]::Combine($thisModuleRoot, $ed.Value), 99 | 'ExternalScript' 100 | ) 101 | } else { # Otherwise, treat it as the name of an exported command. 102 | $loadedModule.ExportedCommands[$ed.Value] 103 | } 104 | if ($extensionCmd) { # If we've found a valid extension command 105 | $extensionCmd # return it. 106 | } 107 | } 108 | } 109 | } 110 | elseif ($loadedModule.Tags -contains $myModuleName) { 111 | foreach ($matchingFile in @(Get-ChildItem (Split-Path $loadedModule.Path) -Recurse) -match '\@\w' -match '\.ps1$') { 112 | if ($matchingFile.Name -match '^\@\w' ) { 113 | $getCmd.Invoke($matchingFile.FullName, 'ExternalScript') 114 | } 115 | } 116 | } 117 | } 118 | $allSources = @() + $atFunctions + $atScripts + $extendedCommands 119 | 120 | $allSources = $allSources | Select-Object -Unique 121 | #endregion Discover Event Sources 122 | } 123 | 124 | process { 125 | foreach ($src in $allSources) { 126 | if ($Name) { 127 | 128 | $ok = 129 | foreach ($n in $Name) { 130 | $src.Name -like "$n" -or 131 | $src.Name -replace '^@' -replace '\.ps1$' -like "$n" 132 | } 133 | 134 | if (-not $ok) { 135 | continue 136 | } 137 | } 138 | 139 | 140 | 141 | if ($Subscription -or $SourceObject) { 142 | if (-not $script:SubscriptionsByEventSource) { continue } 143 | $eventSourceKey = # Then, if the event source was a script, 144 | if ($src -is [Management.Automation.ExternalScriptInfo]) { 145 | $src.Path # the key is the path. 146 | } elseif ($src.Module) { # If it was from a module 147 | $src.Module + '\' + $eventSource.Name # it's the module qualified name. 148 | } else { 149 | $src.Name # Otherwise, it's just the function name. 150 | } 151 | if (-not $script:SubscriptionsByEventSource[$eventSourceKey]) { continue } 152 | if ($Subscription) { 153 | $script:SubscriptionsByEventSource[$eventSourceKey] | 154 | Where-Object { 155 | [Runspace]::DefaultRunspace.Events.Subscribers -contains $_ 156 | } 157 | } else { 158 | if ($script:SubscriptionsByEventSource[$eventSourceKey].SourceObject) { 159 | $script:SubscriptionsByEventSource[$eventSourceKey].SourceObject 160 | } else { 161 | $jobName = $script:SubscriptionsByEventSource[$eventSourceKey].Name 162 | Get-EventSubscriber -SourceIdentifier $jobName -ErrorAction SilentlyContinue | 163 | Select-Object -ExpandProperty SourceObject -ErrorAction SilentlyContinue 164 | } 165 | 166 | } 167 | continue 168 | } 169 | 170 | 171 | 172 | $src.pstypenames.clear() 173 | $src.pstypenames.add('Eventful.EventSource') 174 | if ($Help -and -not $Parameter) { 175 | Get-Help $src.EventSourceID -Full 176 | continue 177 | } 178 | $src 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /.github/workflows/TestAndPublish.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Analyze, Test, Tag, and Publish 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | jobs: 8 | PowerShellStaticAnalysis: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: InstallScriptCop 12 | id: InstallScriptCop 13 | shell: pwsh 14 | run: | 15 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 16 | Install-Module -Name ScriptCop -Repository PSGallery -Force -Scope CurrentUser 17 | Import-Module ScriptCop -Force -PassThru 18 | - name: InstallPSScriptAnalyzer 19 | id: InstallPSScriptAnalyzer 20 | shell: pwsh 21 | run: | 22 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 23 | Install-Module -Name PSScriptAnalyzer -Repository PSGallery -Force -Scope CurrentUser 24 | Import-Module PSScriptAnalyzer -Force -PassThru 25 | - name: InstallPSDevOps 26 | id: InstallPSDevOps 27 | shell: pwsh 28 | run: | 29 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 30 | Install-Module -Name PSDevOps -Repository PSGallery -Force -Scope CurrentUser 31 | Import-Module PSDevOps -Force -PassThru 32 | - name: Check out repository 33 | uses: actions/checkout@v2 34 | - name: RunScriptCop 35 | id: RunScriptCop 36 | shell: pwsh 37 | run: | 38 | $Parameters = @{} 39 | $Parameters.ModulePath = ${env:ModulePath} 40 | foreach ($k in @($parameters.Keys)) { 41 | if ([String]::IsNullOrEmpty($parameters[$k])) { 42 | $parameters.Remove($k) 43 | } 44 | } 45 | Write-Host "::debug:: RunScriptCop $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 46 | & {param([string]$ModulePath) 47 | Import-Module ScriptCop, PSDevOps -PassThru | Out-Host 48 | 49 | if (-not $ModulePath) { 50 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 51 | $ModulePath = ".\$moduleName.psd1" 52 | } 53 | if ($ModulePath -like '*PSDevOps*') { 54 | Remove-Module PSDeVOps # If running ScriptCop on PSDeVOps, we need to remove the global module first. 55 | } 56 | 57 | 58 | $importedModule =Import-Module $ModulePath -Force -PassThru 59 | 60 | $importedModule | Out-Host 61 | 62 | $importedModule | 63 | Test-Command | 64 | Tee-Object -Variable scriptCopIssues | 65 | Out-Host 66 | 67 | foreach ($issue in $scriptCopIssues) { 68 | Write-GitHubWarning -Message "$($issue.ItemWithProblem): $($issue.Problem)" 69 | } 70 | } @Parameters 71 | - name: RunPSScriptAnalyzer 72 | id: RunPSScriptAnalyzer 73 | shell: pwsh 74 | run: | 75 | Import-Module PSScriptAnalyzer, PSDevOps -PassThru | Out-Host 76 | $invokeScriptAnalyzerSplat = @{Path='.\'} 77 | if ($ENV:PSScriptAnalyzer_Recurse) { 78 | $invokeScriptAnalyzerSplat.Recurse = $true 79 | } 80 | $result = Invoke-ScriptAnalyzer @invokeScriptAnalyzerSplat 81 | 82 | foreach ($r in $result) { 83 | if ('information', 'warning' -contains $r.Severity) { 84 | Write-GitHubWarning -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 85 | } 86 | elseif ($r.Severity -eq 'Error') { 87 | Write-GitHubError -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 88 | } 89 | } 90 | TestPowerShellOnLinux: 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: InstallPester 94 | id: InstallPester 95 | shell: pwsh 96 | run: | 97 | $Parameters = @{} 98 | $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} 99 | foreach ($k in @($parameters.Keys)) { 100 | if ([String]::IsNullOrEmpty($parameters[$k])) { 101 | $parameters.Remove($k) 102 | } 103 | } 104 | Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 105 | & {<# 106 | .Synopsis 107 | Installs Pester 108 | .Description 109 | Installs Pester 110 | #> 111 | param( 112 | # The maximum pester version. Defaults to 4.99.99. 113 | [string] 114 | $PesterMaxVersion = '4.99.99' 115 | ) 116 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 117 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber 118 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters 119 | - name: Check out repository 120 | uses: actions/checkout@v2 121 | - name: RunPester 122 | id: RunPester 123 | shell: pwsh 124 | run: | 125 | $Parameters = @{} 126 | $Parameters.ModulePath = ${env:ModulePath} 127 | $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} 128 | $Parameters.NoCoverage = ${env:NoCoverage} 129 | $Parameters.NoCoverage = $parameters.NoCoverage -match 'true'; 130 | foreach ($k in @($parameters.Keys)) { 131 | if ([String]::IsNullOrEmpty($parameters[$k])) { 132 | $parameters.Remove($k) 133 | } 134 | } 135 | Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 136 | & {<# 137 | .Synopsis 138 | Runs Pester 139 | .Description 140 | Runs Pester tests after importing a PowerShell module 141 | #> 142 | param( 143 | # The module path. If not provided, will default to the second half of the repository ID. 144 | [string] 145 | $ModulePath, 146 | # The Pester max version. By default, this is pinned to 4.99.99. 147 | [string] 148 | $PesterMaxVersion = '4.99.99', 149 | 150 | # If set, will not collect code coverage. 151 | [switch] 152 | $NoCoverage 153 | ) 154 | 155 | $global:ErrorActionPreference = 'continue' 156 | $global:ProgressPreference = 'silentlycontinue' 157 | 158 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 159 | if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" } 160 | $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion 161 | $importedModule = Import-Module $ModulePath -Force -PassThru 162 | $importedPester, $importedModule | Out-Host 163 | 164 | $codeCoverageParameters = @{ 165 | CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1" 166 | CodeCoverageOutputFile = ".\$moduleName.Coverage.xml" 167 | } 168 | 169 | if ($NoCoverage) { 170 | $codeCoverageParameters = @{} 171 | } 172 | 173 | 174 | $result = 175 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters 176 | 177 | "::set-output name=TotalCount::$($result.TotalCount)", 178 | "::set-output name=PassedCount::$($result.PassedCount)", 179 | "::set-output name=FailedCount::$($result.FailedCount)" | Out-Host 180 | if ($result.FailedCount -gt 0) { 181 | "::debug:: $($result.FailedCount) tests failed" 182 | foreach ($r in $result.TestResult) { 183 | if (-not $r.Passed) { 184 | "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)" 185 | } 186 | } 187 | throw "::error:: $($result.FailedCount) tests failed" 188 | } 189 | } @Parameters 190 | - name: PublishTestResults 191 | uses: actions/upload-artifact@v2 192 | with: 193 | name: PesterResults 194 | path: '**.TestResults.xml' 195 | if: ${{always()}} 196 | TagReleaseAndPublish: 197 | runs-on: ubuntu-latest 198 | if: ${{ success() }} 199 | steps: 200 | - name: Check out repository 201 | uses: actions/checkout@v2 202 | - name: TagModuleVersion 203 | id: TagModuleVersion 204 | shell: pwsh 205 | run: | 206 | $Parameters = @{} 207 | $Parameters.ModulePath = ${env:ModulePath} 208 | $Parameters.UserEmail = ${env:UserEmail} 209 | $Parameters.UserName = ${env:UserName} 210 | $Parameters.TagVersionFormat = ${env:TagVersionFormat} 211 | $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat} 212 | foreach ($k in @($parameters.Keys)) { 213 | if ([String]::IsNullOrEmpty($parameters[$k])) { 214 | $parameters.Remove($k) 215 | } 216 | } 217 | Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 218 | & {param( 219 | [string] 220 | $ModulePath, 221 | 222 | # The user email associated with a git commit. 223 | [string] 224 | $UserEmail, 225 | 226 | # The user name associated with a git commit. 227 | [string] 228 | $UserName, 229 | 230 | # The tag version format (default value: 'v$(imported.Version)') 231 | # This can expand variables. $imported will contain the imported module. 232 | [string] 233 | $TagVersionFormat = 'v$($imported.Version)', 234 | 235 | # The tag version format (default value: '$($imported.Name) $(imported.Version)') 236 | # This can expand variables. $imported will contain the imported module. 237 | [string] 238 | $TagAnnotationFormat = '$($imported.Name) $($imported.Version)' 239 | ) 240 | 241 | 242 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 243 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 244 | } else { $null } 245 | 246 | 247 | @" 248 | ::group::GitHubEvent 249 | $($gitHubEvent | ConvertTo-Json -Depth 100) 250 | ::endgroup:: 251 | "@ | Out-Host 252 | 253 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 254 | (-not $gitHubEvent.psobject.properties['inputs'])) { 255 | "::warning::Pull Request has not merged, skipping Tagging" | Out-Host 256 | return 257 | } 258 | 259 | 260 | 261 | $imported = 262 | if (-not $ModulePath) { 263 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 264 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 265 | } else { 266 | Import-Module $modulePath -Force -PassThru -Global 267 | } 268 | 269 | if (-not $imported) { return } 270 | 271 | $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) 272 | $existingTags = git tag --list 273 | 274 | @" 275 | Target Version: $targetVersion 276 | 277 | Existing Tags: 278 | $($existingTags -join [Environment]::NewLine) 279 | "@ | Out-Host 280 | 281 | $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion } 282 | 283 | if ($versionTagExists) { 284 | "::warning::Version $($versionTagExists)" 285 | return 286 | } 287 | 288 | if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } 289 | if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } 290 | git config --global user.email $UserEmail 291 | git config --global user.name $UserName 292 | 293 | git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat) 294 | git push origin --tags 295 | 296 | if ($env:GITHUB_ACTOR) { 297 | exit 0 298 | }} @Parameters 299 | - name: ReleaseModule 300 | id: ReleaseModule 301 | shell: pwsh 302 | run: | 303 | $Parameters = @{} 304 | $Parameters.ModulePath = ${env:ModulePath} 305 | $Parameters.UserEmail = ${env:UserEmail} 306 | $Parameters.UserName = ${env:UserName} 307 | $Parameters.TagVersionFormat = ${env:TagVersionFormat} 308 | $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat} 309 | $Parameters.ReleaseAsset = ${env:ReleaseAsset} 310 | $Parameters.ReleaseAsset = $parameters.ReleaseAsset -split ';' -replace '^[''"]' -replace '[''"]$' 311 | foreach ($k in @($parameters.Keys)) { 312 | if ([String]::IsNullOrEmpty($parameters[$k])) { 313 | $parameters.Remove($k) 314 | } 315 | } 316 | Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 317 | & {param( 318 | [string] 319 | $ModulePath, 320 | 321 | # The user email associated with a git commit. 322 | [string] 323 | $UserEmail, 324 | 325 | # The user name associated with a git commit. 326 | [string] 327 | $UserName, 328 | 329 | # The tag version format (default value: 'v$(imported.Version)') 330 | # This can expand variables. $imported will contain the imported module. 331 | [string] 332 | $TagVersionFormat = 'v$($imported.Version)', 333 | 334 | # The release name format (default value: '$($imported.Name) $($imported.Version)') 335 | [string] 336 | $ReleaseNameFormat = '$($imported.Name) $($imported.Version)', 337 | 338 | # Any assets to attach to the release. Can be a wildcard or file name. 339 | [string[]] 340 | $ReleaseAsset 341 | ) 342 | 343 | 344 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 345 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 346 | } else { $null } 347 | 348 | 349 | @" 350 | ::group::GitHubEvent 351 | $($gitHubEvent | ConvertTo-Json -Depth 100) 352 | ::endgroup:: 353 | "@ | Out-Host 354 | 355 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 356 | (-not $gitHubEvent.psobject.properties['inputs'])) { 357 | "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host 358 | return 359 | } 360 | 361 | 362 | 363 | $imported = 364 | if (-not $ModulePath) { 365 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 366 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 367 | } else { 368 | Import-Module $modulePath -Force -PassThru -Global 369 | } 370 | 371 | if (-not $imported) { return } 372 | 373 | $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) 374 | $targetReleaseName = $targetVersion 375 | $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases' 376 | "Release URL: $releasesURL" | Out-Host 377 | $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ 378 | "Accept" = "application/vnd.github.v3+json" 379 | "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' 380 | } 381 | 382 | $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion 383 | 384 | if ($releaseExists) { 385 | "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host 386 | $releasedIt = $releaseExists 387 | } else { 388 | $releasedIt = Invoke-RestMethod -Uri $releasesURL -Method Post -Body ( 389 | [Ordered]@{ 390 | owner = '${{github.owner}}' 391 | repo = '${{github.repository}}' 392 | tag_name = $targetVersion 393 | name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat) 394 | body = 395 | if ($env:RELEASENOTES) { 396 | $env:RELEASENOTES 397 | } elseif ($imported.PrivateData.PSData.ReleaseNotes) { 398 | $imported.PrivateData.PSData.ReleaseNotes 399 | } else { 400 | "$($imported.Name) $targetVersion" 401 | } 402 | draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } 403 | prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } 404 | } | ConvertTo-Json 405 | ) -Headers @{ 406 | "Accept" = "application/vnd.github.v3+json" 407 | "Content-type" = "application/json" 408 | "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' 409 | } 410 | } 411 | 412 | 413 | 414 | 415 | 416 | if (-not $releasedIt) { 417 | throw "Release failed" 418 | } else { 419 | $releasedIt | Out-Host 420 | } 421 | 422 | $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$' 423 | 424 | if ($ReleaseAsset) { 425 | $fileList = Get-ChildItem -Recurse 426 | $filesToRelease = 427 | @(:nextFile foreach ($file in $fileList) { 428 | foreach ($relAsset in $ReleaseAsset) { 429 | if ($relAsset -match '[\*\?]') { 430 | if ($file.Name -like $relAsset) { 431 | $file; continue nextFile 432 | } 433 | } elseif ($file.Name -eq $relAsset -or $file.FullName -eq $relAsset) { 434 | $file; continue nextFile 435 | } 436 | } 437 | }) 438 | 439 | $releasedFiles = @{} 440 | foreach ($file in $filesToRelease) { 441 | if ($releasedFiles[$file.Name]) { 442 | Write-Warning "Already attached file $($file.Name)" 443 | continue 444 | } else { 445 | $fileBytes = [IO.File]::ReadAllBytes($file.FullName) 446 | $releasedFiles[$file.Name] = 447 | Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{ 448 | "Accept" = "application/vnd.github+json" 449 | "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' 450 | } -Body $fileBytes -ContentType Application/octet-stream 451 | $releasedFiles[$file.Name] 452 | } 453 | } 454 | 455 | "Attached $($releasedFiles.Count) file(s) to release" | Out-Host 456 | } 457 | 458 | 459 | 460 | } @Parameters 461 | - name: PublishPowerShellGallery 462 | id: PublishPowerShellGallery 463 | shell: pwsh 464 | run: | 465 | $Parameters = @{} 466 | $Parameters.ModulePath = ${env:ModulePath} 467 | $Parameters.Exclude = ${env:Exclude} 468 | $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$' 469 | foreach ($k in @($parameters.Keys)) { 470 | if ([String]::IsNullOrEmpty($parameters[$k])) { 471 | $parameters.Remove($k) 472 | } 473 | } 474 | Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 475 | & {param( 476 | [string] 477 | $ModulePath, 478 | 479 | [string[]] 480 | $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*') 481 | ) 482 | 483 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 484 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 485 | } else { $null } 486 | 487 | if (-not $Exclude) { 488 | $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*') 489 | } 490 | 491 | 492 | @" 493 | ::group::GitHubEvent 494 | $($gitHubEvent | ConvertTo-Json -Depth 100) 495 | ::endgroup:: 496 | "@ | Out-Host 497 | 498 | @" 499 | ::group::PSBoundParameters 500 | $($PSBoundParameters | ConvertTo-Json -Depth 100) 501 | ::endgroup:: 502 | "@ | Out-Host 503 | 504 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 505 | (-not $gitHubEvent.psobject.properties['inputs'])) { 506 | "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host 507 | return 508 | } 509 | 510 | 511 | $imported = 512 | if (-not $ModulePath) { 513 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 514 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 515 | } else { 516 | Import-Module $modulePath -Force -PassThru -Global 517 | } 518 | 519 | if (-not $imported) { return } 520 | 521 | $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {} 522 | 523 | if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) { 524 | "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host 525 | } else { 526 | 527 | $gk = '${{secrets.GALLERYKEY}}' 528 | 529 | $rn = Get-Random 530 | $moduleTempFolder = Join-Path $pwd "$rn" 531 | $moduleTempPath = Join-Path $moduleTempFolder $moduleName 532 | New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host 533 | 534 | Write-Host "Staging Directory: $ModuleTempPath" 535 | 536 | $imported | Split-Path | 537 | Get-ChildItem -Force | 538 | Where-Object Name -NE $rn | 539 | Copy-Item -Destination $moduleTempPath -Recurse 540 | 541 | $moduleGitPath = Join-Path $moduleTempPath '.git' 542 | Write-Host "Removing .git directory" 543 | if (Test-Path $moduleGitPath) { 544 | Remove-Item -Recurse -Force $moduleGitPath 545 | } 546 | 547 | if ($Exclude) { 548 | "::notice::Attempting to Exlcude $exclude" | Out-Host 549 | Get-ChildItem $moduleTempPath -Recurse | 550 | Where-Object { 551 | foreach ($ex in $exclude) { 552 | if ($_.FullName -like $ex) { 553 | "::notice::Excluding $($_.FullName)" | Out-Host 554 | return $true 555 | } 556 | } 557 | } | 558 | Remove-Item 559 | } 560 | 561 | Write-Host "Module Files:" 562 | Get-ChildItem $moduleTempPath -Recurse 563 | Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" 564 | Publish-Module -Path $moduleTempPath -NuGetApiKey $gk 565 | if ($?) { 566 | Write-Host "Published to Gallery" 567 | } else { 568 | Write-Host "Gallery Publish Failed" 569 | exit 1 570 | } 571 | } 572 | } @Parameters 573 | BuildEventful: 574 | runs-on: ubuntu-latest 575 | if: ${{ success() }} 576 | steps: 577 | - name: Check out repository 578 | uses: actions/checkout@v2 579 | - name: GitLogger 580 | uses: GitLogging/GitLoggerAction@main 581 | id: GitLogger 582 | - name: Use PSSVG Action 583 | uses: StartAutomating/PSSVG@main 584 | id: PSSVG 585 | - name: BuildPipeScript 586 | uses: StartAutomating/PipeScript@main 587 | - name: UseEZOut 588 | uses: StartAutomating/EZOut@master 589 | - name: UseHelpOut 590 | uses: StartAutomating/HelpOut@master 591 | env: 592 | NoCoverage: true 593 | -------------------------------------------------------------------------------- /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 | # The next possibility is an @.ps1 script. 124 | $atFile = "@$($invocationName).ps1" 125 | # This could be in a few places: the current directory first, 126 | foreach ($foundFile in ($pwd, $myRoot | 127 | Get-ChildItem -Recurse | 128 | & { process { 129 | if ($_.Name -eq $atFile) { $_ } 130 | } })) { 131 | 132 | $foundCmd = $getCmd.Invoke($foundFile.FullName, 'ExternalScript') 133 | if ($foundCmd) { return $foundCmd} 134 | } 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 | elseif ($loadedModule.Tags -contains $myModuleName) { 177 | foreach ($matchingFile in @(Get-ChildItem (Split-Path $loadedModule.Path) -Recurse) -match '\@\w' -match '\.ps1$') { 178 | if ($matchingFile.Name -eq $atFile) { 179 | return $getCmd.Invoke($matchingFile.FullName, 'ExternalScript') 180 | } 181 | } 182 | } 183 | } 184 | } 185 | 186 | 187 | } 188 | #endregion Find Event Source 189 | #region Map Dynamic Parameters 190 | if ($eventSource) { 191 | # We only need to map dynamic parameters if there is an event source. 192 | $eventSourceMetaData = [Management.Automation.CommandMetaData]$eventSource 193 | # Determine if we need to offset positional parameters, 194 | $positionOffset = [int]$SourceIDParameterCreated 195 | # then walk over the parameters from that event source. 196 | foreach ($kv in $eventSourceMetaData.Parameters.GetEnumerator()) { 197 | 198 | $attributes = 199 | if ($positionOffset) { # If we had to offset the position of a parameter 200 | @(foreach ($attr in $kv.value.attributes) { 201 | if ($attr -isnot [Management.Automation.ParameterAttribute] -or 202 | $attr.Position -lt 0 203 | ) { 204 | # we can passthru any non-parameter attributes and parameter attributes without position, 205 | $attr 206 | } else { 207 | # but parameter attributes with position need to copied and offset. 208 | $attrCopy = [Management.Automation.ParameterAttribute]::new() 209 | # (Side note: without a .Clone, copying is tedious.) 210 | foreach ($prop in $attrCopy.GetType().GetProperties('Instance,Public')) { 211 | if (-not $prop.CanWrite) { continue } 212 | if ($null -ne $attr.($prop.Name)) { 213 | $attrCopy.($prop.Name) = $attr.($prop.Name) 214 | } 215 | } 216 | 217 | 218 | # Once we have a parameter copy, offset it's position. 219 | $attrCopy.Position+=$positionOffset 220 | $pos = $attrCopy.Position 221 | $attrCopy 222 | } 223 | }) 224 | } else { 225 | $pos = foreach ($a in $kv.value.attributes) { 226 | if ($a.position -ge 0) { $a.position ; break } 227 | } 228 | $kv.Value.Attributes 229 | } 230 | 231 | # Add the parameter and it's potentially modified attributes. 232 | $DynamicParameters.Add($kv.Key, 233 | [Management.Automation.RuntimeDefinedParameter]::new( 234 | $kv.Value.Name, $kv.Value.ParameterType, $attributes 235 | ) 236 | ) 237 | # If the parameter position was bigger than maxPosition, update maxPosition. 238 | if ($pos -ge 0 -and $pos -gt $maxPosition) { $maxPosition = $pos } 239 | } 240 | } 241 | #endregion Map Dynamic Parameters 242 | } 243 | 244 | #endregion Find the Event Source and Map Dynamic Parameters 245 | 246 | 247 | 248 | 249 | #region Optionally Add Source Identifier 250 | # If we don't have an Event Source at this point and we haven't already, 251 | if (-not $eventSource -and -not $SourceIDParameterCreated) { # add the SourceIdentifier parameter. 252 | . $addSourceIdParameter 253 | } 254 | #endregion Optionally Add Source Identifier 255 | 256 | #region Optionally Add InputObject Parameter 257 | # Also, if we don't have an event source 258 | if (-not $eventSource) { 259 | # then we can add an InputObject parameter. 260 | $inputObjectParameter = [Management.Automation.ParameterAttribute]::new() 261 | $inputObjectParameter.ValueFromPipeline = $true 262 | $DynamicParameters.Add("InputObject", 263 | [Management.Automation.RuntimeDefinedParameter]::new( 264 | "InputObject", [PSObject], $inputObjectParameter 265 | ) 266 | ) 267 | 268 | } 269 | #endregion Optionally Add InputObject Parameter 270 | 271 | #region Add Common Parameters 272 | # All calls will always have two additional parameters: 273 | $thenParam = [Management.Automation.ParameterAttribute]::new() 274 | $thenParam.Mandatory = $false #* [ScriptBlock]$then 275 | $thenParam.Position = ++$maxPosition 276 | $thenActionAlias = [Management.Automation.AliasAttribute]::new("Action") 277 | $DynamicParameters.Add("Then", 278 | [Management.Automation.RuntimeDefinedParameter]::new( 279 | "Then", [ScriptBlock], @($thenParam, $thenActionAlias) 280 | ) 281 | ) 282 | 283 | $WhenParam = [Management.Automation.ParameterAttribute]::new() 284 | $whenParam.Position = ++$maxPosition #* [ScriptBlock]$when 285 | $DynamicParameters.Add("When", 286 | [Management.Automation.RuntimeDefinedParameter]::new( 287 | "When", [ScriptBlock], $WhenParam 288 | ) 289 | ) 290 | 291 | $MessageDataParam = [Management.Automation.ParameterAttribute]::new() 292 | $MessageDataParam.Position = ++$maxPosition #* [ScriptBlock]$when 293 | $DynamicParameters.Add("MessageData", 294 | [Management.Automation.RuntimeDefinedParameter]::new( 295 | "MessageData", [PSObject], $MessageDataParam 296 | ) 297 | ) 298 | 299 | $maxTriggerParam = [Management.Automation.ParameterAttribute]::new() 300 | $maxTriggerParam.Position = ++$maxPosition #* [int]$MaxTriggerCount 301 | $DynamicParameters.Add("MaxTriggerCount", 302 | [Management.Automation.RuntimeDefinedParameter]::new( 303 | "MaxTriggerCount", [int], @( 304 | $maxTriggerParam, 305 | [Management.Automation.AliasAttribute]::new("Max"), 306 | [Management.Automation.AliasAttribute]::new("Count") 307 | ) 308 | ) 309 | ) 310 | #endregion Add Common Parameters 311 | 312 | 313 | # Now that we've got all of the dynamic parameters ready 314 | $DynamicParameterNames = $DynamicParameters.Keys -as [string[]] 315 | return $DynamicParameters # return them. 316 | #endregion Handle Input Dynamically 317 | } 318 | 319 | 320 | 321 | process { 322 | 323 | $in = $_ 324 | $registerCmd = $null 325 | $registerParams = @{} 326 | $parameterCopy = @{} + $PSBoundParameters 327 | if ($DebugPreference -ne 'silentlycontinue') { 328 | Write-Debug @" 329 | Watch-Event: 330 | Dynamic Parameters: $DynamicParameterNames 331 | Bound Parameters: 332 | $($parameterCopy | Out-String) 333 | "@ 334 | } 335 | 336 | #region Run Event Source and Map Parameters 337 | if ($eventSource) { # If we have an Event Source, now's the time to run it. 338 | $eventSourceParameter = [Ordered]@{} + $PSBoundParameters # Copy whatever parameters we have 339 | foreach ($toRemove in 'Then','When','SourceIdentifier','MessageData','MaxTriggerCount') { 340 | $eventSourceParameter.Remove($toRemove) 341 | } 342 | $eventSourceOutput = & $eventSource @eventSourceParameter # Then run the generator. 343 | $null = $PSBoundParameters.Remove('SourceIdentifier') 344 | 345 | if ($eventSourceOutput.MessageData) { 346 | $registerParams['MessageData'] = $eventSourceOutput.MessageData 347 | } 348 | $eventSourceMaxTriggerCount = $eventSourceOutput.MaxTriggerCount,$eventSourceOutput.Max,$eventSourceOutput.TriggerCount -as [int[]] -gt 0 349 | 350 | if ($eventSourceMaxTriggerCount) { 351 | $registerParams['MaxTriggerCount'] = $eventSourceMaxTriggerCount[0] 352 | } 353 | 354 | if (-not $eventSourceOutput) { # If it didn't output, 355 | # we're gonna assume it it's gonna by signal by name. 356 | # Set it up so that later code will subscribe to this SourceIdentifier. 357 | $PSBoundParameters["SourceIdentifier"] = $eventSource.Name -replace 358 | '^@' -replace '\.event\.ps1$' -replace '\.ps1$' 359 | } 360 | elseif ($eventSourceOutput.SourceIdentifier) 361 | { 362 | 363 | # If the eventSource said what SourceIdentifier(s) it will send, we will listen. 364 | $PSBoundParameters["SourceIdentifier"] = $eventSourceOutput.SourceIdentifier 365 | } else { 366 | # Otherwise, let's see if the eventSource returned an eventName 367 | $eventName = $eventSourceOutput.EventName 368 | 369 | if (-not $eventName) { # If it didn't, 370 | $eventName = 371 | # Look at the generator script block's attibutes 372 | foreach ($attr in $eventSource.ScriptBlock.Attributes) { 373 | if ($attr.TypeId.Name -eq 'EventSourceAttribute') { 374 | # Return any [Diagnostics.Tracing.EventSource(Name='Value')] 375 | $attr.Name 376 | } 377 | } 378 | } 379 | 380 | 381 | if (-not $eventName) { # If we still don't have an event name. 382 | # check the output for events. 383 | $eventNames = @(foreach ($prop in $eventSourceOutput.psobject.members) { 384 | if ($prop.MemberType -eq 'event') { 385 | $prop.Name 386 | } 387 | }) 388 | 389 | # If there was more than one 390 | if ($eventNames.Count -gt 1) { 391 | # Error out (but output the generator's output, in case that helps). 392 | $eventSourceOutput 393 | Write-Error "Source produced an object with multiple events, but did not specify a '[Diagnostics.Tracing.EventSource(Name=)]'." 394 | return 395 | } 396 | 397 | 398 | $eventName = $eventNames[0] 399 | } 400 | 401 | # Default the Register- command to Register-ObjectEvent. 402 | $registerCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Register-ObjectEvent','Cmdlet') 403 | $registerParams['InputObject']= $eventSourceOutput # Map the InputObject 404 | $registerParams['EventName'] = $eventName # and the EventName. 405 | } 406 | } 407 | #endregion Run Event Source and Map Parameters 408 | 409 | #region Handle -SourceIdentifier and -InputObject 410 | if ($PSBoundParameters['SourceIdentifier']) { # If we have a -SourceIdentifier 411 | if ($PSBoundParameters['InputObject'] -and -not $eventSource) { # and an -InputObject (but not not an eventsource) 412 | # then the register command is Register-ObjectEvent. 413 | $registerCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Register-ObjectEvent','Cmdlet') 414 | 415 | # We map our -SourceIdentifier to Register-ObjectEvent's -EventName, 416 | $registerParams['EventName'] = $PSBoundParameters['SourceIdentifier'] 417 | # and Register-ObjectEvent's InputObject to -InputObject 418 | $registerParams['InputObject'] = $PSBoundParameters['InputObject'] 419 | } 420 | else # If we have a -SourceIdentifier, but no -InputObject 421 | { 422 | # the register command is Register-EngineEvent. 423 | $registerCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Register-EngineEvent','Cmdlet') 424 | # and we map our -SourceIdentifier parameter to Register-EngineEvent's -SourceIdentifier. 425 | $registerParams['SourceIdentifier'] = $PSBoundParameters['SourceIdentifier'] 426 | } 427 | } 428 | #endregion Handle -SourceIdentifier and -InputObject 429 | 430 | #region Handle When and Then 431 | # Assign When and Then for simplicity. 432 | $Then = $PSBoundParameters['Then'] 433 | $when = $PSBoundParameters['When'] 434 | 435 | if ($When) { # If -When was provided 436 | if ($when -is [ScriptBlock]) { # and it was a script 437 | # Rewrite -Then to include -When (this prevents debugging). 438 | # Run -When in a subexpression so it can return from the event handler (not itself) 439 | $then = [ScriptBlock]::Create(@" 440 | `$shouldHandle = `$( 441 | $when 442 | ) 443 | if (-not `$shouldHandle) { return } 444 | $then 445 | "@) 446 | } 447 | } 448 | 449 | if ("$then" -replace '\s') { # If -Then is not blank. 450 | $registerParams["Action"] = $Then # this maps to the -Action parameter the Register- comamnd. 451 | } 452 | #endregion Handle When and Then 453 | 454 | #region Handle MaxTriggerCount and MessageData 455 | if ($parameterCopy['MaxTriggerCount']) { 456 | $registerParams['MaxTriggerCount'] = $parameterCopy['MaxTriggerCount'] 457 | } 458 | 459 | if ($parameterCopy['MessageData']) { 460 | $registerParams['MessageData'] = $parameterCopy['MessageData'] 461 | } 462 | #endregion 463 | 464 | #region Subscribe to Event 465 | 466 | # Now we create the event subscription. 467 | $eventSubscription = 468 | 469 | 470 | # If there's a Register- command. 471 | if ($registerCmd) { 472 | if ($registerParams["EventName"]) { # and we have -EventName 473 | $evtNames = $registerParams["EventName"] 474 | $registerParams.Remove('EventName') 475 | 476 | # Call Register-Object event once 477 | foreach ($evtName in $evtNames) { # for each event name ( 478 | # give it a logical SourceIdentifier. 479 | $sourceId = $registerParams["InputObject"].GetType().FullName + ".$evtName" 480 | $existingSubscribers = @(Get-EventSubscriber -SourceIdentifier "${sourceID}*") 481 | if ($existingSubscribers) { # (If subscribers exist, increment the source ID)) 482 | $maxSourceId = 0 483 | foreach ($es in $existingSubscribers) { 484 | if ($es.SourceIdentifier -match '\.\d+$') { 485 | $esId = [int]($matches.0 -replace '\.') 486 | if ($esId -gt $maxSourceId) { 487 | $maxSourceId = $esId 488 | } 489 | } 490 | } 491 | $sourceID = $sourceId + ".$($maxSourceId + 1)" 492 | } 493 | 494 | # Then call Register-ObjectEvent 495 | & $registerCmd @registerParams -EventName $evtName -SourceIdentifier $sourceId 496 | } 497 | } 498 | elseif ($registerParams["SourceIdentifier"]) # 499 | { 500 | $sourceIdList = $registerParams["SourceIdentifier"] 501 | $null = $registerParams.Remove('SourceIdentifier') 502 | # If we don't have an action, don't run anything (this will let the events "bubble up" to the runspace). 503 | if ($registerParams.Action) { 504 | # If we do have an action, call Register-Engine event with each source identifier. 505 | foreach ($sourceId in $sourceIdList) { 506 | & $registerCmd @registerParams -SourceIdentifier $sourceId 507 | } 508 | } 509 | } 510 | } 511 | #endregion Subscribe to Event 512 | 513 | 514 | #region Keep track of Subscriptions by EventSource 515 | # Before we're done, let's track what we subscribed to. 516 | 517 | if ($eventSource) { 518 | # Make sure a cache exists. 519 | if (-not $script:SubscriptionsByEventSource) { 520 | $script:SubscriptionsByEventSource = @{} 521 | } 522 | $eventSourceKey = # Then, if the event source was a script, 523 | if ($eventSource -is [Management.Automation.ExternalScriptInfo]) { 524 | $eventSource.Path # the key is the path. 525 | } elseif ($eventSource.Module) { # If it was from a module 526 | $eventSource.Module.ToString() + '\' + $eventSource.Name # it's the module qualified name. 527 | } else { 528 | $eventSource.Name # Otherwise, it's just the function name. 529 | } 530 | $script:SubscriptionsByEventSource[$eventSourceKey] = 531 | if ($eventSubscription -is [Management.Automation.Job]) { 532 | Get-EventSubscriber -SourceIdentifier $eventSubscription.Name -ErrorAction SilentlyContinue 533 | } else { 534 | $eventSubscription 535 | } 536 | 537 | $eventSourceInitializeAttribute= $eventSource.ScriptBlock.Attributes | 538 | Where-Object TypeID -EQ ([ComponentModel.InitializationEventAttribute]) 539 | if ($eventSourceOutput.InitializeEvent -and $eventSourceOutput.InitializeEvent -is [string]) { 540 | $eventSourceOutput.$($eventSourceOutput.InitializeEvent).Invoke() 541 | } 542 | elseif ($eventSourceOutput.InitializeEvent -and $eventSourceOutput.InitializeEvent -is [ScriptBlock]) { 543 | $this = $sender = $eventSourceOutput 544 | & ([ScriptBlock]::Create($eventSourceInitializeAttribute.EventName)) 545 | } 546 | elseif ($eventSourceInitializeAttribute.EventName -match '^[\w\-]+$') { 547 | $eventSourceOutput.($eventSourceInitializeAttribute.EventName).Invoke() 548 | } else { 549 | $this = $sender = $eventSourceOutput 550 | & ([ScriptBlock]::Create($eventSourceInitializeAttribute.EventName)) 551 | } 552 | } 553 | 554 | #endregion Keep track of Subscription 555 | 556 | 557 | #region Passthru if needed 558 | if ($myInv.PipelinePosition -lt $myInv.PipelineLength) { # If this is not the last step of the pipeline 559 | $in # pass down the original object. (This would let one set of arguments pipe to multiple calls) 560 | } 561 | else { 562 | 563 | } 564 | #endregion Passthru if needed 565 | } 566 | } 567 | --------------------------------------------------------------------------------