I've cobbled together an example of a runspace popping up a Windows form and a timer closing it after 3 seconds. See this example:
$runspace = [runspacefactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$PowerShell = [PowerShell]::Create().AddScript({
$objForm = New-Object System.Windows.Forms.Form
$Timer = New-Object System.Windows.Forms.Timer
$Timer.Interval = 1000
$script:countdown = 3
$Timer.Add_Tick({
--$script:countdown
if ($script:countdown -lt 0)
{
$Timer.Dispose();
$objForm.Dispose();
}
})
$Timer.Start()
$objForm.ShowDialog() | Out-Null
})
$PowerShell.Runspace = $runspace
$null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action {
param([System.Management.Automation.PowerShell] $ps)
$state = $EventArgs.InvocationStateInfo.State
if ($state -in 'Completed', 'Failed', 'Stopped') {
write-host $state
$ps.Runspace.Dispose()
[GC]::Collect()
}
}
$AsyncHandle = $PowerShell.BeginInvoke()
$PowerShell.EndInvoke($AsyncHandle)
$PowerShell.Dispose()
write-host "Continue with the rest of my script"
The idea is to open the form in a separate runspace and continue with my script processing. However, it doesn't continue the script unless i remove the lines:
$PowerShell.EndInvoke($AsyncHandle)
$PowerShell.Dispose()
But if I remove these lines, the script will currently leave a handle open to the runspace.
My question is, can I auto-cleanup the runspace when the runspace is complete somewhere here?
if ($state -in 'Completed', 'Failed', 'Stopped') {
#call EndInvoke on this instance??
$ps.Runspace.Dispose()
[GC]::Collect()
}
Thanks.
UPDATE - Attempt 1
$runspace = [runspacefactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$PowerShell = [PowerShell]::Create().AddScript({
$objForm = New-Object System.Windows.Forms.Form
$Timer = New-Object System.Windows.Forms.Timer
$Timer.Interval = 1000
$script:countdown = 3
$Timer.Add_Tick({
--$script:countdown
if ($script:countdown -lt 0)
{
$Timer.Dispose();
$objForm.Dispose();
}
})
$Timer.Start()
$objForm.ShowDialog() | Out-Null
})
$PowerShell.Runspace = $runspace
$null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action {
param([System.Management.Automation.PowerShell] $ps)
if($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') {
$state['Instance'].EndInvoke($state['Handle'])
$state['Instance'].Runspace.Dispose()
$state['Instance'].Dispose()
}
}
$state = @{
Instance = $PowerShell
Handle = $PowerShell.BeginInvoke()
}
write-host "Continue with the rest of my script"
$state['Instance'].Runspace.RunspaceStateInfo.State
start-sleep -Seconds 10
$state['Instance'].Runspace.RunspaceStateInfo.State
UPDATE - Attempt 2
function Show-Form() {
$runspace = [runspacefactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$PowerShell = [PowerShell]::Create().AddScript({
$objForm = New-Object System.Windows.Forms.Form
$Timer = New-Object System.Windows.Forms.Timer
$Timer.Interval = 1000
$script:countdown = 3
$Timer.Add_Tick({
--$script:countdown
if ($script:countdown -lt 0)
{
$Timer.Dispose();
$objForm.Dispose();
}
})
$Timer.Start()
$objForm.ShowDialog() | Out-Null
})
$PowerShell.Runspace = $runspace
$null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action {
param([System.Management.Automation.PowerShell] $ps)
if($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') {
$global:state['Instance'].EndInvoke($global:state['Handle'])
$global:state['Instance'].Runspace.Dispose()
$global:state['Instance'].Dispose()
}
}
$global:state = @{
Instance = $PowerShell
Handle = $PowerShell.BeginInvoke()
}
}
Show-Form
#do other stuff
do {
#write-host
$global:state['Instance'].Runspace.RunspaceStateInfo.State
start-sleep 1
} while($global:state['Instance'].Runspace.RunspaceStateInfo.State -ne 'closed')
Attempt 3
function Show-Form() {
$runspace = [runspacefactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "UseNewThread"
$runspace.Open()
$PowerShell = [PowerShell]::Create().AddScript({
$objForm = New-Object System.Windows.Forms.Form
$objForm.ShowDialog() | Out-Null
})
$PowerShell.Runspace = $runspace
$state = @{
Instance = $PowerShell
Handle = $PowerShell.BeginInvoke()
}
$Jobs.Add($state) | Out-Null
$null = Register-ObjectEvent -InputObject $state.Instance -MessageData $state.Handle -EventName InvocationStateChanged -Action {
param([System.Management.Automation.PowerShell] $ps)
if($ps.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') {
write-host "closing instance " $ps
write-host "closing handle " $Event.MessageData
$ps.Runspace.Close()
$ps.Runspace.Dispose()
$ps.EndInvoke($Event.MessageData)
$ps.Dispose()
[GC]::Collect()
}
write-host "finish"
}
}
Show-Form
Show-Form
write-host "other stuff"
Looks like your original code is based on this answer, which calls $ps.Runspace.Dispose()
from inside the -Action script block, as in your original attempt. However, the enclosing [powershell]
instance is retained there, for a later (blocking) call to .EndInvoke()
from the main thread, and for potential later reuse for additional parallel tasks.
(In the end, that main thread should call .Dispose()
on the [powershell]
instance, and I've updated the linked answer to make that clear.)
Per your feedback, the problem you ran into was that not fully disposing of all the PowerShell SDK-related objects caused you script to hang when compiled into an .exe
file with ps2exe.ps1
.
While you have found a solution, let me offer a simpler one, which:
Uses the default runspace that is used when you create a [powershell]
instance without assigning an externally created runspace instance to its .Runspace
property.
In that case, the only cleanup needed is to call .Dispose()
on that [powershell]
instance from inside the -Action
script block.
Add-Type -AssemblyName System.Windows.Forms
function Show-Form() {
# Create a PowerShell instance with a default runspace
# and add code for showing a WinForms form modally.
$PowerShell = [PowerShell]::Create().AddScript({
$objForm = New-Object System.Windows.Forms.Form
$null = $objForm.ShowDialog()
})
# Start the runspace task asynchronously, i.e. display the form and move on.
# Since the task produces no output, there's *no* need to save the async
# handle returned and to call .EndInvoke() before calling .Dispose()
$null = $PowerShell.BeginInvoke()
# Register for the PowerShell instance's invocation-state-changed event.
$null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action {
param([System.Management.Automation.PowerShell] $ps)
if ($ps.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') {
# Disposing of the PowerShell object as a whole is seemingly sufficient,
# if its default runspace was used, which is then *implicitly* disposed too.
$ps.Dispose()
}
}
}
# Create two forms running in a separate thread (runspace) each.
Show-Form
Show-Form
# You're free to do other things here, including exiting the
# script right away.
# If you exit before the forms were closed, they will stay open, and
# cleanup will not happen until they're closed.
write-host "other stuff"