Reputation: 36738
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]?
Upvotes: 3
Views: 1048
Reputation: 201832
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
}
Upvotes: 2
Reputation: 36738
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.
Upvotes: 0
Reputation: 68301
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.
Upvotes: 0
Reputation: 47832
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.
Start-Test -Block { ps | select -first 9 }
Start-Test -Block { ps | select -first 9 } -Async
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.
Upvotes: 0