Hi folks
Another WTF…
Let’s consider the following scenario:
Script1.ps1
#requires -version 2.0
[CmdletBinding()]
param
(
)
$script:ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }
Trap { throw $_ }
& "$(PSScriptRoot)\Script2.ps1"
# TODO: Invoke Script2.ps1 asynchronously
Script2.ps1
#requires -version 2.0
[CmdletBinding()]
param
(
)
$script:ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }
Trap { throw $_ }
& "$(PSScriptRoot)\Script3.ps1"
Script3.ps1
#requires -version 2.0
[CmdletBinding()]
param
(
)
$script:ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }
Trap { throw $_ }
"Invoking Script3.ps1"
Now we want to modify Script1.ps1 to run Script2.ps1 asynchronously
Let’s modify line #13
$job = Start-Job -FilePath "$(PSScriptRoot)\Script2.ps1" $job | Wait-Job | Receive-Job
If we run Script1.ps1 it fails with
Receive-Job : Cannot bind argument to parameter 'Path' because it is an empty string.
At C:\dev\Script1.ps1:14 char:30
+ $job | Wait-Job | Receive-Job <<<<
+ CategoryInfo : InvalidData: (:String) [Split-Path], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAllowed,Microsoft.PowerShell.Commands.SplitPathCommand
Stack trace is not helpful at all. But I narrowed down the issue to the line #10 of Script2.ps1
function PSScriptRoot { $MyInvocation.ScriptName | Split-Path }
The problem here that $MyInvocation.ScriptName is null
This happened because Start-Job reads the content of Script2.ps1 and executes it not in a script scope but as a separate ScriptBlock.
To emulate the problem we can use
$code = Get-Content -Path .\Script2.ps1 | Out-String
$scriptBlock = [ScriptBlock]::Create($code)
& $scriptBlock
And this fails with the same error
We cannot get rid of the this PSScriptRoot function because it is required to invoke Script3.ps1
So to fix it let’s try another approach
$job = Start-Job -ScriptBlock { & "$(PSScriptRoot)\Script2.ps1" }
$job | Wait-Job | Receive-Job
And this fails with
The term 'PSScriptRoot' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of th e name, or if a path was included, verify that the path is correct and try again.
At C:\dev\Script1.ps1:14 char:1
+ <<<< $job | Wait-Job | Receive-Job
+ CategoryInfo : ObjectNotFound: (PSScriptRoot:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
This happens because $(PSScriptRoot) function is accessed inside the started job and is not available at this stage. So we need to expand ”$(PSScriptRoot)\Script2.ps1” before the job started
$script2 = "$(PSScriptRoot)\Script2.ps1"
$job = Start-Job -ScriptBlock { & $script2 }
$job | Wait-Job | Receive-Job
And this fails with
The expression after '&' in a pipeline element produced an invalid object. It must result in a command name, script block or CommandInfo object.
At C:\dev\Script1.ps1:15 char:1
+ <<<< $job | Wait-Job | Receive-Job
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : BadExpression
The reason for that is the fact that we are using $script2 variable inside the started job and this variable is not available.
We need to construct a ScriptBlock from the string
$script2 = "$(PSScriptRoot)\Script2.ps1"
$scriptBlock = [ScriptBlock]::Create($script2)
$job = Start-Job -ScriptBlock $scriptBlock
$job | Wait-Job | Receive-Job
And this works as expected
So our final cleaned version
$job = Start-Job -ScriptBlock ([ScriptBlock]::Create("$(PSScriptRoot)\Script2.ps1"))
$job | Wait-Job | Receive-Job
Oh man… That was not easy…