When is a ScriptBlock not a ScriptBlock?

I don't mean to sound too sweet with a question, but it really is a question. Consider the following two functions, defined in the Test.psm1 PowerShell module installed under $ env: PSModulePath:

function Start-TestAsync
{
    [CmdletBinding()]
    param([ScriptBlock]$Block, [string]$Name = '')   
    Start-Job { Start-Test -Name $using:Name -Block $using:Block }
}

function Start-Test
{
    [CmdletBinding()]
    param([ScriptBlock]$Block, [string]$Name = '')
    # do some work here, including this:
    Invoke-Command -ScriptBlock $Block
}

      

After importing the module, I can run a synchronous function ...

PS> Start-Test -Name "My Test" -Block { ps | select -first 9 }

      

... and displays the corresponding Get-Process result.

However, when I try to run the async version ...

PS> $testJob=Start-TestAsync -Name "My Test" -Block { ps | select -first 9 }

      

... and then view its output ...

PS> Receive-Job $testJob

      

... fails to just inject a parameter into the Start-Test function, reporting that it cannot convert String to ScriptBlock. This way it -Block $using:Block

is passing a string, not a ScriptBlock!

After some experimentation, I found a workaround. If I change Start-Test so that the type of the $ Block parameter is [string] instead of [ScriptBlock] - and then convert that string back to a block to issue the Invoke-Command ...

function Start-Test
{
    [CmdletBinding()]
    param([string]$Block, [string]$Name = '')
    $myBlock = [ScriptBlock]::Create($Block)
    Invoke-Command -ScriptBlock $myBlock
}

      

Then I get the correct result when I execute the same commands from above:

PS> $testJob=Start-TestAsync -Name "My Test" -Block { ps | select -first 9 }
PS> Receive-Job $testJob

      

Is the scope using

working correctly in my original example (converting a ScriptBlock to a string)? The limited documentation on it ( about_Remote_Variables , about_Scopes ) offers little guidance. Ultimately, is there a way to make Start-Test work when its $ Block parameter is entered as [ScriptBlock]?

+3


source to share


4 answers


While it's good to know (thanks @KeithHill) that what I saw was a known issue - sorry I meant "by design" - my real question was not answered ("Ultimately, is there a way to make Start -Test when its $ Block parameter is entered as [ScriptBlock]? ")

Yesterday I received an unexpected answer:



function Start-TestAsync
{
    [CmdletBinding()]
    param([ScriptBlock]$Block, [string]$Name = '')   
    Start-Job {
        $myBlock = [ScriptBlock]::Create($using:Block);
        Start-Test -Name $using:Name -Block $myBlock  }
}

function Start-Test
{
    [CmdletBinding()]
    param([ScriptBlock]$Block, [string]$Name = '')   
    # do some work here, including this:
    Invoke-Command -ScriptBlock $Block
}

      

Note that in Start-TestAsync, I internally enable serialization ($ using: Block) by converting the ScriptBlock to String and then immediately converting it (Create) to ScriptBlock, and then can safely pass this to run-test as a genuine ScriptBlock. For me, this is a significant improvement over the workaround in my question, because now the public APIs for both functions are correct.

0


source


This seems to be by design: https://connect.microsoft.com/PowerShell/feedback/details/685749/passing-scriptblocks-to-the-job-as-an-argument-cannot-process-argument-transformation -on-parameter

The workaround (from the link above) is to use [ScriptBlock]::Create()

:

This is because the $ ScriptToNest script gets converted to a string due to the way PowerShell serialization works. You can work around this explicitly by creating a script block. Replace the param () block in your $ OuterScript block with the following ($ ip is the input):

[scriptblock]$OuterScriptblock = {
param($ip)
[ScriptBlock]$ScriptToRun = [ScriptBlock]::Create($ip)

      



This will be your job (as you already found):

function Start-TestAsync
{
    [CmdletBinding()]
    param([ScriptBlock]$Block, [string]$Name = '')
    Start-Job { Start-Test -Name $using:Name -Block $using:Block }
}

function Start-Test
{
    [CmdletBinding()]
    param($Block, [string]$Name = '')
    # do some work here, including this:
    $sb = [ScriptBlock]::Create($Block)
    Invoke-Command -ScriptBlock $sb
}

      

+2


source


I realize this doesn't quite answer your question, but I think you could simplify this by putting it in a single function:

function Start-Test
{
    [CmdletBinding()]
    param(
        [ScriptBlock]$Block, 
        [string]$Name = '',
        [Switch]$Async
    )
    # do some work here, including this:
    Invoke-Command -ScriptBlock $Block -AsJob:$Async
}

      

Since it Invoke-Command

might already start the job for you, you can force your function to accept the switch -Async

and then pass its value to -AsJob

.

Synchronous call

Start-Test -Block { ps | select -first 9 }

      

Calling asynchronous

Start-Test -Block { ps | select -first 9 } -Async

      

Speculation

As for what is going on with the actual thing, I'm not sure, but I think it might have something to do with nesting script blocks, although I can't do the right test at the moment.

0


source


I suppose the reason you are seeing this is because the purpose of using $ is to expand the values ​​of local variables inside the script block before it is used on the remote system - it doesn't actually create those variables in the remote session. The closest thing to scriptblock is command text.

0


source







All Articles