2

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!

Jurm
  • 21
  • 2
  • 2
    Too much code to go through, answering the immediate question, "How do you reuse local functions in different PowerShell runspaces?", store the definition of the function as a string in a variable then pass that variable to the runspace and assign it using `$function:funcDefinition` as shown in [this answer](https://stackoverflow.com/questions/61273189/how-to-pass-a-custom-function-inside-a-foreach-object-parallel) – Santiago Squarzon Aug 27 '22 at 00:16
  • 1
    @SantiagoSquarzon Thanks a ton, apologies for the code wall, I was only going to include a snippet and got carried away lol. Just walking through that answer you posted and hopefully I'll be able to get something workable out of it. Worst case ontario I'll just deal with some messy functions thrown everywhere. Thanks again. – Jurm Aug 27 '22 at 00:28
  • 1
    That answers shows how it can be done with `ForEach-Object -Parallel` though the concept is the same, the cmdlet uses runspaces behind the scenes. – Santiago Squarzon Aug 27 '22 at 00:33
  • Where you able to sort this around? If not @me and I'll post a simple example of a function being passed to runspaces. Since you're already using a runspacepool it would be even better to pass the function definition the the session state proxy as you're already doing with the sync hash – Santiago Squarzon Aug 27 '22 at 18:41
  • @SantiagoSquarzon To be honest I didn't quite get it working as I wanted and just included the function in the runspaces that needed it until I had more time to research the link you posted -- but if you had an example that would be fantastic and really appreciated – Jurm Aug 28 '22 at 04:34

1 Answers1

0

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)
}
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37