Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Full Change Log

- [v1.0.1](https://github.com/Windos/BurntToast/releases/download/v1.0.1/BurntToast.zip)

- Bug Fixes

- OnActivated events are "sticky"

- See #256 by [Windos](https://github.com/Windos)

- [1.0.0](https://github.com/Windos/BurntToast/releases/download/v1.0.0/BurntToast.zip)

- Breaking Changes
Expand Down
2 changes: 2 additions & 0 deletions Help/Submit-BTNotification.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Submits a completed toast notification for display.
The `Submit-BTNotification` function submits a completed toast notification to the operating system's notification manager for display.
This function supports advanced scenarios such as event callbacks for user actions or toast dismissal, sequence numbering to ensure correct update order, unique identification for toast replacement, expiration control, and direct Action Center delivery.

When a script block is supplied for any of the event actions (`ActivatedAction`, `DismissedAction`, or `FailedAction`), the function ensures that only one registration for a logically identical handler is allowed per notification event type. This is accomplished by normalizing and hashing the script block; the resulting hash uniquely identifies the action for event registration purposes. Attempting to register the same handler multiple times for a given event will not create a duplicate subscription, but instead will produce an informative warning.

If the `-ReturnEventData` switch is used and any event action scriptblocks are supplied (`ActivatedAction`, `DismissedAction`, `FailedAction`),
the `$Event` automatic variable from the event will be assigned to `$global:ToastEvent` before invoking your script block.
You can override the variable name used for event data by specifying `-EventDataVariable`.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ See the [Chocolatey community package](https://chocolatey.org/packages/burnttoas

## Releases

### [v1.0.1](https://github.com/Windos/BurntToast/releases/download/v1.0.1/BurntToast.zip)

#### Bug Fixes

- OnActivated events are "sticky"

- See #256 by [Windos](https://github.com/Windos)

### [v1.0.0](https://github.com/Windos/BurntToast/releases/download/v1.0.0/BurntToast.zip)

#### Breaking Changes
Expand Down
31 changes: 31 additions & 0 deletions Tests/PrivateHelpers.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ BeforeAll {

. "$PSScriptRoot/../src/Private/Add-PipelineObject.ps1"
. "$PSScriptRoot/../src/Private/Optimize-BTImageSource.ps1"
. "$PSScriptRoot/../src/Private/Get-BTScriptBlockHash.ps1"
}

Describe 'Add-PipelineObject' {
Expand Down Expand Up @@ -69,4 +70,34 @@ Describe 'Optimize-BTImageSource' {
}
}
}
}
Describe 'Get-BTScriptBlockHash' {
It 'returns a consistent hash for identical scriptblocks' {
$sb1 = { Write-Host 'foo'; "bar" }
$sb2 = { Write-Host 'foo'; "bar" }
$hash1 = Get-BTScriptBlockHash $sb1
$hash2 = Get-BTScriptBlockHash $sb2
$hash1 | Should -BeExactly $hash2
}
It 'is whitespace-agnostic for logically identical scriptblocks' {
$sb1 = {Write-Host 'foo'; "bar"}
$sb2 = {
Write-Host
'foo'
;"bar"
}
$hash1 = Get-BTScriptBlockHash $sb1
$hash2 = Get-BTScriptBlockHash $sb2
$hash1 | Should -BeExactly $hash2
}
It 'distinguishes truly different scriptblocks' {
$sb1 = { Write-Host 'foo' }
$sb2 = { Write-Host 'bar' }
Get-BTScriptBlockHash $sb1 | Should -Not -BeExactly (Get-BTScriptBlockHash $sb2)
}
It 'hashes scriptblocks containing only whitespace the same as empty' {
$sb1 = { }
$sb2 = { }
Get-BTScriptBlockHash $sb1 | Should -BeExactly (Get-BTScriptBlockHash $sb2)
}
}
31 changes: 31 additions & 0 deletions Tests/Submit-BTNotification.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,35 @@ Describe 'Submit-BTNotification' {
{ Submit-BTNotification -Content $mockContent -UniqueIdentifier 'Toast001' -WhatIf } | Should -Not -Throw
}
}
Context 'event handler deduplication/registration' {
It 'registers a unique action event without error' {
$mockContent = [Activator]::CreateInstance([Microsoft.Toolkit.Uwp.Notifications.ToastContent])
$action = { Write-Host "Activated!" }
{ Submit-BTNotification -Content $mockContent -ActivatedAction $action -WhatIf } | Should -Not -Throw
}
It 'warns and does not throw on duplicate action registration' {
$mockContent = [Activator]::CreateInstance([Microsoft.Toolkit.Uwp.Notifications.ToastContent])
$action = { Write-Host "Activated!" }

$transcriptPath = "$env:TEMP\BTTranscript-$PID.txt"
Start-Transcript -Path $transcriptPath -Force | Out-Null
try {
$null = Submit-BTNotification -Content $mockContent -ActivatedAction $action -WhatIf 2>&1
} finally {
Stop-Transcript | Out-Null
}

$transcript = Get-Content $transcriptPath -Raw
$transcript | Should -Match "Duplicate or conflicting OnActivated ScriptBlock event detected"
Remove-Item $transcriptPath -Force
}
It 'allows distinct handler actions without deduplication warning' {
$mockContent = [Activator]::CreateInstance([Microsoft.Toolkit.Uwp.Notifications.ToastContent])
$a1 = { Write-Host "A" }
$a2 = { Write-Host "B" }
$null = Submit-BTNotification -Content $mockContent -ActivatedAction $a1 -WhatIf 2>&1
$output = Submit-BTNotification -Content $mockContent -ActivatedAction $a2 -WhatIf 2>&1
$output | Should -Not -Contain "Duplicate or conflicting OnActivated ScriptBlock event detected"
}
}
}
10 changes: 8 additions & 2 deletions src/BurntToast.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'BurntToast.psm1'
ModuleVersion = '1.0.0'
ModuleVersion = '1.0.1'
# Can only use CompatiblePSEditions if PowerShellVersion is set to 5.1, not sure about limiting this to that version yet.
# CompatiblePSEditions = @('Desktop')
GUID = '751a2aeb-a68f-422e-a2ea-376bdd81612a'
Expand Down Expand Up @@ -38,7 +38,13 @@
LicenseUri = 'https://github.com/Windos/BurntToast/blob/main/LICENSE'
ProjectUri = 'https://github.com/Windos/BurntToast'
IconUri = 'https://rawcdn.githack.com/Windos/BurntToast/3dd8dd7457552056da4bbd27880f8283e1116395/Media/BurntToast-Logo.png'
ReleaseNotes = '# 1.0.0
ReleaseNotes = '# 1.0.1

* Bug Fixes
* OnActivated events are "sticky"
* See #256 by [Windos](https://github.com/Windos)

# 1.0.0

* Breaking Changes
* Custom Audio Path Removed: Support for custom audio file sources has been eliminated.
Expand Down
35 changes: 35 additions & 0 deletions src/Private/Get-BTScriptBlockHash.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
function Get-BTScriptBlockHash {
<#
.SYNOPSIS
Returns a normalized SHA256 hash for a ScriptBlock.

.DESCRIPTION
Converts the ScriptBlock to string, collapses whitespace, trims, lowercases, and returns SHA256 hash.
Used to uniquely identify ScriptBlocks for event registration scenarios.

.PARAMETER ScriptBlock
The [scriptblock] to hash.

.INPUTS
System.Management.Automation.ScriptBlock

.OUTPUTS
String (SHA256 hex)

.EXAMPLE
$hash = Get-BTScriptBlockHash { Write-Host 'Hello' }
#>
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[scriptblock]$ScriptBlock
)
process {
# Remove all whitespace and semicolons for robust logical identity
$normalized = ($ScriptBlock.ToString() -replace '[\s;]+', '').ToLowerInvariant()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($normalized)
$sha = [System.Security.Cryptography.SHA256]::Create()
$hashBytes = $sha.ComputeHash($bytes)
-join ($hashBytes | ForEach-Object { $_.ToString('x2') })
}
}
55 changes: 50 additions & 5 deletions src/Public/Submit-BTNotification.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
The Submit-BTNotification function submits a completed toast notification to the operating system's notification manager for display.
This function supports advanced scenarios such as event callbacks for user actions or toast dismissal, sequence numbering to ensure correct update order, unique identification for toast replacement, expiration control, and direct Action Center delivery.

When an action ScriptBlock is supplied (Activated, Dismissed, or Failed), a normalized SHA256 hash of its content is used to generate a unique SourceIdentifier for event registration.
This prevents duplicate handler registration for the same ScriptBlock, warning if a duplicate registration is attempted.

If the -ReturnEventData switch is used and any event action scriptblocks are supplied (ActivatedAction, DismissedAction, FailedAction),
the $Event automatic variable from the event will be assigned to $global:ToastEvent before invoking your script block.
You can override the variable name used for event data by specifying -EventDataVariable. If supplied, the event data will be assigned to the chosen global variable in your event handler (e.g., -EventDataVariable 'CustomEvent' results in $global:CustomEvent).
Expand Down Expand Up @@ -174,7 +177,6 @@

if ($ReturnEventData -or $EventDataVariable -ne 'ToastEvent') {
$EventReturn = '$global:{0} = $Event' -f $EventDataVariable

if ($ActivatedAction) {
$Action_Activated = [ScriptBlock]::Create($EventReturn + "`n" + $Action_Activated.ToString())
}
Expand All @@ -187,15 +189,58 @@
}

if ($Action_Activated) {
Register-ObjectEvent -InputObject $CompatMgr -EventName OnActivated -Action $Action_Activated | Out-Null
try {
$ActivatedHash = Get-BTScriptBlockHash $Action_Activated
$activatedParams = @{
InputObject = $CompatMgr
EventName = 'OnActivated'
Action = $Action_Activated
SourceIdentifier = "BT_Activated_$ActivatedHash"
ErrorAction = 'Stop'
}
Register-ObjectEvent @activatedParams | Out-Null
} catch {
Write-Warning "Duplicate or conflicting OnActivated ScriptBlock event detected: Activation action not registered. $_"
}
<#
EDGE CASES / NOTES
- Hash collisions: In the rare event that two different ScriptBlocks normalize to the same text, they will share a SourceIdentifier and not both be registered.
- Only ScriptBlocks are handled: if a non-ScriptBlock is supplied where an action is expected, registration will fail.
- Actions with dynamic or closure content: If `ToString()` outputs identical strings for two blocks with different closure state, only one event will register.
- User warnings: Any error during event registration (including duplicate) triggers a user-facing warning instead of otherwise disrupting notification flow.
#>
}
if ($Action_Dismissed -or $Action_Dismissed) {
if ($Action_Dismissed -or $Action_Failed) {
if ($Script:ActionsSupported) {
if ($Action_Dismissed) {
Register-ObjectEvent -InputObject $Toast -EventName Dismissed -Action $Action_Dismissed | Out-Null
try {
$DismissedHash = Get-BTScriptBlockHash $Action_Dismissed
$dismissedParams = @{
InputObject = $Toast
EventName = 'Dismissed'
Action = $Action_Dismissed
SourceIdentifier = "BT_Dismissed_$DismissedHash"
ErrorAction = 'Stop'
}
Register-ObjectEvent @dismissedParams | Out-Null
} catch {
Write-Warning "Duplicate or conflicting Dismissed ScriptBlock event detected: Dismissed action not registered. $_"
}
}
if ($Action_Failed) {
Register-ObjectEvent -InputObject $Toast -EventName Failed -Action $Action_Failed | Out-Null
try {
$FailedHash = Get-BTScriptBlockHash $Action_Failed
$failedParams = @{
InputObject = $Toast
EventName = 'Failed'
Action = $Action_Failed
SourceIdentifier = "BT_Failed_$FailedHash"
ErrorAction = 'Stop'
}
Register-ObjectEvent @failedParams | Out-Null
} catch {
Write-Warning "Duplicate or conflicting Failed ScriptBlock event detected: Failed action not registered. $_"
}
}
} else {
Write-Warning $Script:UnsupportedEvents
Expand Down