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]?
source to share
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.
source to share
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
}
source to share
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.
source to share
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.
source to share