How do you reuse local functions in different PowerShell runspaces?

Viewed 73

I've been playing around and learning about runspaces and Winforms and am wondering how I'd go about reusing a function for each different runspace I make. I can get it to work by including the full script of the function in each scriptblock, but I was looking to just have the function written once and reference it whenever I make a new runspace.

Here's what I currently have:

$hash = [hashtable]::Synchronized(@{})
$Script:Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList))
# Our main GUI Form object
$hash.Form = New-Object System.Windows.Forms.Form

# Results that come back from the query get saved here
$script:hash.ServerResults = $null

# Buttons
$hash.QueryServerButton = New-Object System.Windows.Forms.Button

# Button properties
$hash.QueryServerButton.Location = New-Object System.Drawing.Point(40,300)
$hash.QueryServerButton.Size = New-Object System.Drawing.Size (105,40)
$hash.QueryServerButton.Text = "Get Servers"
$hash.QueryServerButton.Add_Click({
    PerformRunspaceAction($QueryServer)
})
$hash.ServerTab.Controls.Add($hash.QueryServerButton)

$QueryServer = {
    $connectionString = "Server=xxx\xxx;Database=xxxx;Trusted_Connection=True;"
    $sql = @"
    SELECT * FROM DB_DB;
    "@

    do {
        Start-Sleep -Seconds 30  
        $script:hash.ServerResults = sqlExecuteRead $connectionString $sql $pars
    } while ($true)
}


Function PerformRunspaceAction {
            param($scriptRun)
    
            $newRunspace = [runspacefactory]::CreateRunspace()
            $newRunspace.ApartmentState = "STA"
            $newRunspace.ThreadOptions = "ReuseThread"
            $newRunspace.Open()
            $newRunspace.SessionStateProxy.SetVariable("hash", $hash)
    
            $Powershell = [powershell]::Create().AddScript($scriptRun)
            $Powershell.Runspace = $newRunspace
            [void]$Script:Jobs.Add((
                [PSCustomObject]@{
                    PowerShell = $PowerShell
                    Runspace = $PowerShell.BeginInvoke()
                }
            ))
    }

function sqlExecuteRead($connectionString, $sqlCommand, $pars) {

    $connection = new-object system.data.SqlClient.SQLConnection($connectionString)
    $connection.Open()
    $command = new-object system.data.sqlclient.sqlcommand($sqlCommand, $connection)

    if ($pars -and $pars.Keys) {
       foreach($key in $pars.keys) {
        # avoid injection in varchar parameters
            $par = $command.Parameters.Add("@$key", [system.data.SqlDbType]::VarChar, 512);
            $par.Value = $pars[$key];
        }
    }

    $adapter = New-Object System.Data.sqlclient.sqlDataAdapter $command
    $dataset = New-Object System.Data.DataSet
    $adapter.Fill($dataset) | Out-Null
    $connection.Close()
    return $dataset.tables[0].rows
}

It doesn't work how it's laid out there, but if I move the sqlExecuteRead function into $QueryServer it works fine. The problem is I want to use this function in many other runspaces as well and was hoping not to copypaste it everywhere. Apologies if the code appears to be missing pieces/use case doesn't make sense as I had to cut quite a bit out. :)

Any help is appreciated!

1 Answers

Here is one way you can initialize your runspaces including pre-defined functions in your local session. Below code adds the functions to a InitialSessionState instance, in this case we will be using a Default2 session state as this is a lighter version. Then the RunspacePool is instantiated using the CreateRunspacePool(Int32, Int32, InitialSessionState, PSHost) constructor which allows us to include our pre-defined InitialSessionState instance with the functions in it.

using namespace System.Management.Automation.Runspaces

function Say-Hello1 {param($i) "Hello from function 1 - Runspace $i" }
function Say-Hello2 {param($i) "Hello from function 2 - Runspace $i" }

[SessionStateFunctionEntry[]] $funcs = Get-Command Say-Hello* | ForEach-Object {
    [SessionStateFunctionEntry]::new($_.Name, $_.Definition)
}

$iss    = [initialsessionstate]::CreateDefault2()
$iss.Commands.Add($funcs)
$rspool = [runspacefactory]::CreateRunspacePool(1, 10, $iss, $Host)
$rspool.Open()

$runspaces = 1..10 | ForEach-Object {
    $ps = [powershell]::Create().AddScript({
        param($i)

        Start-Sleep 2

        Say-Hello1 $i
        Say-Hello2 $i

    }).AddParameters(@{ i = $_ })

    $ps.RunspacePool = $rspool

    @{
        Instance    = $ps
        AsyncResult = $ps.BeginInvoke()
    }
}

foreach($runspace in $runspaces) {
    $runspace.Instance.EndInvoke($runspace.AsyncResult)
    $runspace.Instance.Dispose()
}

$rspool.Dispose()

In the example above you will notice we can use Get-Command to target the desired functions that will be initialized with the runspaces, this cmdlet allows for wildcards and also multiple function names. Supposing you have the functions Get-This, Set-That and Add-SomethingElse you could also create the SessionStateFunctionEntry enumerable with the exact function's Name:

[SessionStateFunctionEntry[]] $funcs = Get-Command Get-This, Set-That, Add-SomethingElse | ForEach-Object {
    [SessionStateFunctionEntry]::new($_.Name, $_.Definition)
}
Related