Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Expand variable with scriptblock inside variable in a loop with runspaces

$RunspaceCollection = @()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = @({'somecode'},{'someothercode'})

Foreach ($test in $case) {

    $finalcode= {
        Invoke-Command -ScriptBlock [scriptblock]::create($code[$test])
    }.GetNewClosure()

    $Powershell = [PowerShell]::Create().AddScript($finalcode)
    $Powershell.RunspacePool = $RunspacePool
    [Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property @{
        Runspace = $PowerShell.BeginInvoke()
        PowerShell = $PowerShell
}}

The finalcode variable doesn't expand when the GetNewClosure() happens, so $code[$test] gets into the runspace instead of actual code and I can't get my desired results. Any advice?

Using the method from the answer I'm getting this in the runspace, but it doesn't execute properly. I can confirm that my command is loaded into runspace (at least while in debugger inside runspace I can execute it without dot sourcing)

[System.Management.Automation.PSSerializer]::Deserialize('<ObjsVersion="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <SBK> My-Command -Par1 "egweg" -Par2 "qwrqwr" -Par3 "wegweg"</SBK>
</Objs>')

This is what I see in debugger in runspace

Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> 

Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> s

Stopped at: </Objs>')) }
like image 933
4c74356b41 Avatar asked Sep 04 '26 22:09

4c74356b41


2 Answers

The problem with your code is that AddScript method of PowerShell class is expecting a string, not ScriptBlock. And any closure will be lost when you convert ScriptBlock to string. To solve this, you can pass argument to script with AddArgument method:

$RunspaceCollection = New-Object System.Collections.Generic.List[Object]
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = @({'somecode'},{'someothercode'})
$finalcode= {
    param($Argument)
    Invoke-Command -ScriptBlock ([scriptblock]::create($Argument))
}

Foreach ($test in $case) {
    $Powershell = [PowerShell]::Create().AddScript($finalcode).AddArgument($code[$test])
    $Powershell.RunspacePool = $RunspacePool
    $RunspaceCollection.Add((New-Object -TypeName PSObject -Property @{
        Runspace = $PowerShell.BeginInvoke()
        PowerShell = $PowerShell
    }))
}
like image 125
user4003407 Avatar answered Sep 06 '26 12:09

user4003407


I'm not sure if there's a better way off the top of my head, but you can replace the variables yourself with serialized versions of the same.

You can't use $Using: in this case, but I wrote a function that replaces all $Using: variables manually.

My use case was with DSC, but it would work in this case as well. It allows you to still write your script blocks as scriptblocks (not as strings), and supports variables with complex types.

Here's the code from my blog (also available as a GitHub gist):

function Replace-Using {
[CmdletBinding(DefaultParameterSetName = 'AsString')]
[OutputType([String], ParameterSetName = 'AsString')]
[OutputType([ScriptBlock], ParameterSetName = 'AsScriptBlock')]
param(
    [Parameter(
        Mandatory,
        ValueFromPipeline
    )]
    [String]
    $Code ,

    [Parameter(
        Mandatory,
        ParameterSetName = 'AsScriptBlock'
    )]
    [Switch]
    $AsScriptBlock
)

    Process {
        $cb = {
            $m = $args[0]
            $ser = [System.Management.Automation.PSSerializer]::Serialize((Get-Variable -Name $m.Groups['var'] -ValueOnly))
            "`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
        }
        $newCode = [RegEx]::Replace($code, '\$Using:(?<var>\w+)', $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)

        if ($AsScriptBlock.IsPresent) {
            [ScriptBlock]::Create($newCode)
        } else {
            $newCode
        }
    }
}

A better way for me to do this replacement would probably be to use the AST instead of string replacement, but.. effort.

Your Use Case

$finalcode= {
    Invoke-Command -ScriptBlock [scriptblock]::create($Using:code[$Using:test])
} | Replace-Using

For better results you might assign a variable first and then just insert that:

$value = [scriptblock]::Create($code[$test])

$finalcode= {
    Invoke-Command -ScriptBlock $Using:value
} | Replace-Using
like image 23
briantist Avatar answered Sep 06 '26 13:09

briantist