3

I do not mean to sound too cute with the question, but that really is the question at hand. Consider the following two functions defined in a PowerShell module Test.psm1 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 then run the synchronous function...

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

...and it displays appropriate output from Get-Process.

However, when I attempt to run the asynchronous version...

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

...and then review its output...

PS> Receive-Job $testJob

... it fails at just bringing in the parameter to the Start-Test function, reporting it cannot convert a String to a ScriptBlock. Thus, -Block $using:Block is passing a String rather than a ScriptBlock!

After some experimentation, I did find a workaround. If I modify 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 feed to Invoke-Command...

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

I then obtain the correct result when I run the same commands from above:

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

Is the using scope working correctly in my initial 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 typed as a [ScriptBlock]?

4 Answers 4

2

This is apparently 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 happens because the $ScriptToNest scriptblock is getting converted into a string because of how PowerShell serialization works. You can work around this by explicitly creating the scriptblock. Replace the param() block in your $OuterScriptblock with the following ($ip is the input):

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

This would be your work-around (as you've 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
}
Sign up to request clarification or add additional context in comments.

2 Comments

Perhaps I am missing something, Keith, but that is essentially what I reported in my question, no? I see only one cosmetic difference, in that you omit a type on the $Block parameter entirely where I had it as a string... I do appreciate the Connect reference, though.
Yup, but for future reference for other folks it is nice to have the work-around in one of the answers. :-)
0

I realize this doesn't quite answer your question, but I think you could simplify this a lot by putting it into one 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 Invoke-Command can already start a job for you, you can have your function accept an -Async switch, then pass its value to the -AsJob switch.

Invoke Synchronous

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

Invoke Asynchronous

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

Speculation

As for why that actual thing you're seeing is happening, I'm not certain, but I think it might be related to nesting the script blocks, though I'm unable to do a proper test at the moment.

1 Comment

Good suggestion, @briantist! Unfortunately, that would not work in my particular circumstances only because I have more than the single Invoke-Command that needs to be run as a job.
0

I believe the reason you're seeing this is that the purpose of $using is to expand the values of of local variables inside the script block prior to it being used on the remote system - it doesn't actually create those variables in the the remote session. The closest thing a scriptblock has to a value is it's command text.

Comments

0

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

The answer came to me suddenly last night:

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
}

Notice that in Start-TestAsync I internally allow the serialization to occur ($using:Block), converting the ScriptBlock to a String, then immediately re-convert it (Create) to a ScriptBlock, and can then safely pass that on to Start-Test as a genuine ScriptBlock. To me, this is a significant improvement over the workaround in my question because now the public APIs on both functions are correct.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.