Last active
December 14, 2024 01:00
-
-
Save alx9r/601734436bc3128aa900c085a399318a to your computer and use it in GitHub Desktop.
Test setup for testing PowerShell object lifetime and cleanup when the runspace in which the executing script block is stopped.
This file contains hidden or 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
# define a type with the following features: | |
# - can wait for a signal to complete contruction | |
# - records constructed and disposed flags such that it is beyond object lifetime | |
Add-Type @' | |
public class WaitingDisposableMock : System.IDisposable { | |
// global statics for tracking object lifetime | |
public static bool Disposed = false; | |
public static bool Constructed = false; | |
public WaitingDisposableMock(System.Threading.ManualResetEventSlim waitSignal) { | |
Disposed = false; | |
Constructed = true; | |
// Wait to complete construction if the signal exists. | |
if (null != waitSignal) { | |
waitSignal.Wait(); | |
} | |
} | |
public void Dispose() { | |
Disposed = true; | |
} | |
} | |
'@ | |
function Invoke-TestRun { | |
<# | |
.SYNOPSIS | |
Stop a partly-executed scriptblock and output information about progress, object lifetime, and cleanup. | |
#> | |
param( | |
[Parameter(Mandatory,ValueFromPipeline)] | |
[ValidateSet('begin{} before ctor','begin{} inside ctor','begin{} after ctor','clean{try{}}','clean{finally{}}')] | |
[string] | |
# The point in the scriptblock at which to way for .StopProcessing(). | |
$WaitPoint | |
) | |
process { | |
# initialize the mock's globals | |
[WaitingDisposableMock]::Disposed = $false | |
[WaitingDisposableMock]::Constructed = $false | |
# set up the wait signal | |
$waitSignals = [System.Collections.Concurrent.ConcurrentDictionary[string,System.Threading.ManualResetEventSlim]]::new() | |
$waitSignals[$WaitPoint] = [System.Threading.ManualResetEventSlim]::new() | |
# create the log | |
$log = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() | |
$job = | |
Start-ThreadJob ` | |
-argu $waitSignals,$log ` | |
-script { | |
param($waitSignals,$log) | |
begin { | |
# Write progress to the log. | |
$log.Enqueue( 'begin{} before ctor') | |
# If the signal exists, wait for it. . {} ensures execution is interrupted here on .StopProcessing() | |
($waitSignals['begin{} before ctor'])?.Wait() ; . {} | |
# Most of the remainder of this scriptblock are pairs of log/wait line like above. | |
$log.Enqueue( 'begin{} inside ctor') | |
# Construct the mock object. Waiting occurs inside the constructor when that signal exists. | |
$obj = [WaitingDisposableMock]::new($waitSignals['begin{} inside ctor']) ; . {} | |
$log.Enqueue( 'begin{} after ctor') | |
($waitSignals['begin{} after ctor'])?.Wait() ; . {} | |
$log.Enqueue( 'begin{} complete') | |
} | |
clean { | |
<# | |
Workaround clean{} block stopping if the runspace is stopped in the clean block. | |
Note the following: | |
- Interruption can only begin at one, specific point. | |
- The pipeline that is running when stop is invoked is stopped even if it is clean{} or finally{}. | |
- When that happens in clean{try{}}, it's a problem because cleanup is interrupted. So it is also run in clean{try{}}. | |
- If $clean is interrupted during try{}, then it will run again in finally{}. | |
- If $clean is interrupted during finally{}, then it has already run without interruption in try{}. | |
#> | |
try { | |
# define cleanup | |
$clean = { $obj | ? {$_} | % Dispose } | |
$log.Enqueue( 'clean{try{}}') | |
($waitSignals['clean{try{}}'])?.Wait() ; . {} | |
# invoke cleanup, this might be interrupted | |
. $clean | |
$log.Enqueue('clean{try{}} complete') | |
} | |
finally { | |
$log.Enqueue( 'clean{finally{}}') | |
($waitSignals['clean{finally{}}'])?.Wait() ; . {} | |
# invoke cleanup, this might be interrupted | |
. $clean | |
$log.Enqueue('clean{finally{}} complete') | |
} | |
$log.Enqueue('clean{} complete') | |
} | |
} | |
# Wait for the job to start. | |
Start-Sleep -Milliseconds 100 | |
# By this point, the job should have executed to the WaitPoint. | |
# Signal the job to stop. | |
$job.StopJobAsync() | |
# Wait for Stopping to begin. | |
Start-Sleep -Milliseconds 100 | |
# By this point, the Stopping should have begun. | |
# Signal execution to continue from the WaitPoint. | |
$waitSignals[$WaitPoint].Set() | |
# Wait for the job to complete. | |
$job | Wait-Job | Out-Null | |
# Clean up. | |
$job | Remove-Job | |
# Output the results of the test run. | |
[pscustomobject]@{ | |
# The results are OK if neither or both of Constructed and Disposed completed. | |
OK = if ([WaitingDisposableMock]::Disposed -xor [WaitingDisposableMock]::Constructed) { '❌' } else { '✅' } | |
WaitPoint = $WaitPoint | |
Constructed = [WaitingDisposableMock]::Constructed | |
Disposed = [WaitingDisposableMock]::Disposed | |
# Compute the latest point reach in each of the begin{} and clean{} blocks. | |
BeginProgress = $log | ? {$_ -like 'begin{}*'} | Select-Object -Last 1 | |
CleanTryProgress = $log | ? {$_ -like 'clean{try{}}*'} | Select-Object -Last 1 | |
CleanFinallyProgress = $log | ? {$_ -like 'clean{finally{}}*'} | Select-Object -Last 1 | |
} | |
} | |
} | |
# Invoke and format the test output for each of the WaitPoints. | |
'begin{} before ctor', | |
'begin{} inside ctor', | |
'begin{} after ctor' , | |
'clean{try{}}' , | |
'clean{finally{}}' | | |
Invoke-TestRun | | |
Format-Table |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment