Skip to content

Instantly share code, notes, and snippets.

@alx9r
Last active September 19, 2022 21:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alx9r/a52d1432f443672eeb2cd99707a076d7 to your computer and use it in GitHub Desktop.
Save alx9r/a52d1432f443672eeb2cd99707a076d7 to your computer and use it in GitHub Desktop.
Proof-of-concept of an error- and stop-safe cleanup pattern.
# 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