Last active
September 19, 2022 21:12
-
-
Save alx9r/a52d1432f443672eeb2cd99707a076d7 to your computer and use it in GitHub Desktop.
Proof-of-concept of an error- and stop-safe cleanup pattern.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Proof-of-concept of an error- and stop-safe cleanup pattern. | |
<# | |
Note that accepting pipeline input for these functions is not supported | |
because per https://gist.github.com/alx9r/f81cf64f50a016edda4e92968bdb9c3b | |
* there is no way to ensure cleanup when an upstream command breaks, throws, | |
continues, or throws a statement-terminating error, and | |
* there is no way to determine from a mid-pipeline command whether an exception | |
thrown downstream will cause the end of the invokation or will be swallowed by | |
an upstream command. | |
These two points means that it is not possible to know whether to clean up unless | |
the function performing the cleanup is the first element in the pipeline. Accordingly, | |
These commands can only be used as the first element in the pipeline. | |
#> | |
Set-Alias usingObject Invoke-UsingObject | |
function Invoke-UsingObject | |
{ | |
param | |
( | |
# the scriptblock that produces the object that will be used | |
[Parameter(Position=1,Mandatory)] | |
[scriptblock] | |
$ObjectProducer, | |
# This is the scriptblock that is invoked after $ScriptBlock. It can | |
# be used to enact context-specific cleanup. | |
[scriptblock] | |
$Afterward, | |
# the user scriptblock | |
[Parameter(Position=2)] | |
[scriptblock] | |
$ScriptBlock | |
) | |
# produce the object we will be using | |
$object = & $ObjectProducer | |
if ( (-not $Afterward) -and ($object -isnot [System.IDisposable] ) ) | |
{ | |
Write-Warning 'Invoke-UsingObject was called on a non-IDisposable without -Afterward. Some cleanup might not have been performed.' | |
} | |
try | |
{ | |
if ( $ScriptBlock ) | |
{ | |
# invoke the user scriptblock with the object we will be using | |
# Pass $object so it can be used. | |
# Use % so that the scriptblock can have side-effects | |
# and so that $_ contains $object without process{} | |
# in $Afterward | |
% $ScriptBlock -InputObject $object | |
} | |
else | |
{ | |
# emit the object for use downstream | |
$object | |
} | |
} | |
finally | |
{ | |
# cleanup | |
if ( $Afterward ) | |
{ | |
# Invoke the context-specific cleanup scriptblock. | |
# Prevent any output to the pipeline from $Afterward. | |
% $Afterward -InputObject $object | Out-Null | |
} | |
if ( $object -is [System.IDisposable] ) | |
{ | |
$object.Dispose() | |
} | |
} | |
} | |
function Afterward { | |
# This function is really just syntactic sugar for the case | |
# where no object is used. | |
param | |
( | |
[Parameter(Position=1,Mandatory)] | |
[scriptblock] | |
$Afterward, | |
[Parameter(Mandatory)] | |
[scriptblock] | |
$First | |
) | |
Invoke-UsingObject {} -Afterward $Afterward -ScriptBlock $First | |
} | |
# a stub IDisposable for tracing .Dispose() | |
class f : System.IDisposable { | |
Dispose() { Write-Host 'f.dispose()' } | |
} | |
### exercise Invoke-UsingObject and Afterward | |
'=== usingObject {[IDisposable]::new()}' | |
usingObject { [f]::new() } { | |
"do some work using $_" | |
} | |
'=== usingObject {object} -Afterward {cleanup}' | |
usingObject { 'some object' } -Afterward { | |
Write-Host "do some context-specific cleanup using $_" | |
} { | |
"do some work using $_" | |
} | |
'=== usingObject {[nonIDisposable]} without -Afterward' | |
usingObject { 'some object' } { | |
"do some work using $_" | |
} | |
'=== usingObject {[IDisposable]} -Afterward {}' | |
usingObject { [f]::new() } -Afterward { | |
Write-Host "do some context-specific cleanup using $_" | |
} { | |
"do some work using $_" | |
} | |
'=== Afterward {} -First {}' | |
Afterward { Write-Host 'clean up' } -First { | |
'do some work' | |
} | |
function throws { | |
param ( [Parameter(ValueFromPipeline)] $InputObject ) | |
process { throw 'something' } | |
} | |
function breaks { | |
param ( [Parameter(ValueFromPipeline)] $InputObject ) | |
process { break } | |
} | |
function continues { | |
param ( [Parameter(ValueFromPipeline)] $InputObject ) | |
process { continue } | |
} | |
function TerminateStatement { | |
[CmdletBinding()] | |
param | |
( | |
[Parameter(ValueFromPipeline)]$x | |
) | |
process | |
{ | |
$PSCmdlet.ThrowTerminatingError( | |
[System.Management.Automation.ErrorRecord]::new( | |
'Statement-Terminating Error', | |
'id', | |
0, | |
$null | |
) | |
) | |
} | |
} | |
function blocks { | |
param ( [Parameter(ValueFromPipeline)] $InputObject ) | |
process { Wait-Event 'bogus' } | |
} | |
'===throws downstream' | |
try | |
{ | |
usingObject { [f]::new() } | throws | |
} | |
catch{} | |
'===throws in scriptblock' | |
try | |
{ | |
usingObject { [f]::new() } { throws } | |
} | |
catch{} | |
'===breaks downstream' | |
do { | |
usingObject { [f]::new() } | breaks | |
} while (1) | |
'===breaks in scriptblock' | |
do { | |
usingObject { [f]::new() } { breaks } | |
} while (1) | |
'===continues downstream' | |
foreach ( $i in 1) | |
{ | |
usingObject { [f]::new() } | continues | |
Write-Host 'this should get skipped' | |
} | |
'===continues in scriptblock' | |
foreach ( $i in 1) | |
{ | |
usingObject { [f]::new() } { continues } | |
Write-Host 'this should get skipped' | |
} | |
'===Ctrl-C downstream' | |
usingObject { [f]::new() } | blocks | |
'===Ctrl-C in scriptblock' | |
usingObject { [f]::new() } { blocks } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment