Skip to content

RunSpaces In PowerShell

The concept of Runspaces is built upon the .NET threading model, which allows PowerShell to execute multiple scripts or commands in parallel. This is achieved by creating separate instances of the PowerShell engine, each running in its own thread, thus not interfering with the primary PowerShell session or other Runspaces.

Utilizing Runspaces effectively requires an understanding of threading and synchronization, as data sharing between threads must be handled carefully to avoid race conditions and ensure thread safety. The synchronized hashtable, as demonstrated in the provided script, is a prime example of a thread-safe data structure that facilitates communication between Runspaces.


How To Create RunSpace and PowerShell Instance and Reuse Them

# Display the number of the runspaces before operation
(Get-Runspace).count

# Create a synchronized hashtable for inter-runspace communication
$SyncHash = [System.Collections.Hashtable]::Synchronized(@{})

# Create a new runspace
$GUIRunSpace = [System.Management.Automation.RunSpaces.RunSpaceFactory]::CreateRunSpace()
$GUIRunSpace.ApartmentState = 'STA'
$GUIRunSpace.ThreadOptions = 'ReuseThread'

# Create a new PowerShell object
$GUIPowerShell = [System.Management.Automation.PowerShell]::Create()
# Assign the runspace to the PowerShell object's Runspace property
$GUIPowerShell.RunSpace = $GUIRunSpace
# Open the runspace
$GUIRunSpace.Open()

# Make the synchronized hashtable available in the runspace
$GUIRunSpace.SessionStateProxy.SetVariable('SyncHash', $SyncHash)

# Add a script to the PowerShell object so that it can run inside the runspace
[System.Void]$GUIPowerShell.AddScript({
        Write-Output -InputObject '1st output'
    })

# Invoke the PowerShell object asynchronously and store the resulting handle in a variable
$GUIAsyncObject = $GUIPowerShell.BeginInvoke()

# End the asynchronous operation and display the output
$GUIPowerShell.EndInvoke($GUIAsyncObject)

# Add another script to the PowerShell object to run, replacing the previous script added to the object.
# The runspace is still open
[System.Void]$GUIPowerShell.AddScript({
        Write-Output -InputObject '2nd output'
    })

# Again invoke the PowerShell object asynchronously and store the resulting handle in a variable
$GUIAsyncObject = $GUIPowerShell.BeginInvoke()

# End the asynchronous operation and display the output
$GUIPowerShell.EndInvoke($GUIAsyncObject)

# Close and dispose of the runspace and PowerShell object
$GUIPowerShell.Dispose()
$GUIRunSpace.Close()
$GUIRunSpace.Dispose()

# Display the number of the runspaces after operation
(Get-Runspace).count


How To Handle RunSpace Events Asynchronously From The Parent RunSpace

This example demonstrates how to create a runspace that runs a GUI thread asynchronously and based on the events happening in the GUI thread, different actions are taken in the parent thread. The parent thread is responsible for handling the events generated by the GUI thread, such as button clicks, window closures, and errors. At the end of the operation, the runspace is closed and disposed of, ensuring no leftover runspaces or jobs.


# Get the count of the RunSpaces before the operation to compare it with the count after the operation
(Get-Runspace).count

# Creating a synchronized hashtable to store shared data between the two runspaces
$SyncedHashtable = [System.Collections.Hashtable]::Synchronized(@{})

# Define the XAML code for WPF GUI
$SyncedHashtable.XAML = [System.Xml.XmlDocument]@'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MaxWidth="600" WindowStartupLocation="CenterScreen" SizeToContent="WidthAndHeight">
<Button Name="Button1" Content="Press Me"/>
</Window>
'@

# Assigning the parent runspace's host to the $SyncedHashtable.Host property.
# It will be used to detect or rather, refer back to the parent runspace from inside of the GUI runspace.
# This is crucial for the event communication between the two runspaces.
$SyncedHashtable.Host = $Host

$RunSpace = [System.Management.Automation.RunSpaces.RunSpaceFactory]::CreateRunspace()
$RunSpace.ApartmentState = 'STA'
$RunSpace.ThreadOptions = 'ReuseThread'

$RunSpace.Open()
$RunSpace.SessionStateProxy.SetVariable('SyncedHashtable', $SyncedHashtable)

$PowerShell = [System.Management.Automation.PowerShell]::Create()
$PowerShell.Runspace = $RunSpace

[System.Void]$PowerShell.AddScript({

        try {
            # Add the required assembly for WPF
            Add-Type -AssemblyName PresentationFramework

            $Reader = New-Object -TypeName 'System.Xml.XmlNodeReader' -ArgumentList $SyncedHashtable.XAML
            $SyncedHashtable.Window = [System.Windows.Markup.XamlReader]::Load( $Reader )

            # Find the button object in the XAML
            [System.Windows.Controls.Button]$SyncedHashtable.Button1 = $SyncedHashtable.Window.FindName('Button1')

            # Add a click event to the button
            $SyncedHashtable.Button1.Add_Click({
                    $SyncedHashtable.Host.Runspace.Events.GenerateEvent('Button1Clicked', $null, 'Button Click Event', $null)
                })

            # Add a closed event to the window
            $SyncedHashtable.Window.Add_Closed({
                    $SyncedHashtable.Host.Runspace.Events.GenerateEvent('WindowClosed', $null, 'Sender', $null)
                })

            # Throw a dummy error to test the async error handling
            # throw 'Test Error'

            # Show the GUI
            $SyncedHashtable.Window.ShowDialog()
        }
        catch {
            $SyncedHashtable.ErrorMessage = $_.Exception.Message
            $SyncedHashtable.Host.Runspace.Events.GenerateEvent('ErrorsOccurred', $null, $null, $null)
        }
    })

# Start the GUI PowerShell instance asynchronously
$AsyncHandle = $PowerShell.BeginInvoke()

# You can inspect the events that the 'Register-EngineEvent' cmdlet receives here
# $Button1ClickedEvent = Get-Event -SourceIdentifier 'Button1Clicked'
# $WindowClosedEvent = Get-Event -SourceIdentifier 'WindowClosed'
# $ErrorsOccurredEvent = Get-Event -SourceIdentifier 'ErrorsOccurred'

# Register an event for the button click
$Button1ClickedSub = Register-EngineEvent -SourceIdentifier 'Button1Clicked' -Action {
    param (
        $Sender
    )
    Write-Host -Object $Sender
}

# Register an event for the window closure
$WindowClosedSub = Register-EngineEvent -SourceIdentifier 'WindowClosed' -Action {
    param (
        $Sender
    )
    Write-Host -Object 'The GUI has been closed.'

    # Remove the event subscription and the job for the button click event since the GUI Windows was closed
    Unregister-Event -SubscriptionId $Button1ClickedSub.Id
    Remove-Job -InstanceId $Button1ClickedSub.InstanceId

    # Remove the event subscription and the job for the errors occurred event since the GUI Windows was closed
    Unregister-Event -SubscriptionId $ErrorsOccurredSub.Id
    Remove-Job -InstanceId $ErrorsOccurredSub.InstanceId

    # Close the runspace and dispose of it
    $RunSpace.Close()
    $RunSpace.dispose()

    # Remove the event subscription and the job of the current event subscription
    Unregister-Event -SubscriptionId $WindowClosedSub.Id
    Remove-Job -InstanceId $WindowClosedSub.InstanceId
}

# Register an event for the errors occurred in the GUI RunSpace
$ErrorsOccurredSub = Register-EngineEvent -SourceIdentifier 'ErrorsOccurred' -Action {
    Write-Host -Object "Errors Occurred: $($SyncedHashtable.errorMessage)"
}

# Get the count of the runspaces after the operation to see there is no leftover runspace
(Get-Runspace).count

# There won't be any leftover jobs or event subscriptions once the GUI window is closed
# Get-EventSubscriber
# Get-Job