'$PSScriptRoot empty when called with Powershell ScheduledJob using -Filepath parameter

The automatic variable $PSScriptRoot isn't populating when using a Powershell ScheduledJob (not to be confused with a ScheduledTask).

A ScheduledJob with the -Filepath Parameter specifying a local script:

-Filepath "C:\Scriptpath\Script.ps1"

Fails to populate $PSScriptRoot.

Get-Job reveals that it isn't running the script, but is instead outputting the script like it's reading a .txt file. false

A ScheduledJob with the -Scriptblock and the & Call Operator specifying a local script:

-Scriptblock {& "C:\Scriptpath\Script.ps1"}

Succeeds to populate $PSScriptRoot.

Script.ps1 can be as simple as

$PSScriptRoot | out-file "C:\Test.txt"

The issue appears to be how the file is being executed by the Job in Task Scheduler, rather than the syntax errors in the .ps1. Since the same script works if called through the -Scriptblock parameter. In either case, the script is being executed because Out-File will create a blank txt file.

Both usages appear to be in accordance with Microsoft's Documentation:

https://docs.microsoft.com/en-us/powershell/module/psscheduledjob/register-scheduledjob?view=powershell-5.1

Any guidance on the behavior would be helpful.

Edit:

What I am noticing is that Get-Job, which shows the "command" that the scheduled job is executing.

If using -Scriptblock. Get-Job shows the exact contents of script block, like above.

& "C:\Scriptpath\Script.ps1"

If using -Filepath. Get-Job shows the contents of the file.

$PSScriptRoot | out-file "C:\Test.txt"

It's like it's copying the contents of the file and then invoking them. This would explain why $PSScriptRoot is failing to be populated.

....~test noises~

$PWD | Out-File "C:\file.txt"

In the file shows that $PWD happens to be the $Home variable of the user account that the scheduled job is being executed with. I think I'm on to something.



Solution 1:[1]

The documentation is not very clear about it, but as it turns out, both -Filepath and -Scriptblock options will execute script blocks. And therefore, the automatic variable $PSScriptRoot won't be properly initialized, since this only happens when the caller is a script. In both cases, the caller is the TaskScheduler which will call Powershell.exe with some extra arguments, including the -Command which in turn, will load a job definition from serialized XML, that will ultimately load the contents of the given file.

The way it works under the hood is, when using the -Filepath option of the Register-ScheduledJob, the cmdlet creates a somewhat special ScheduledTask that will execute Powershell with the following arguments:

-NoLogo -NonInteractive -WindowStyle Hidden 
-Command "Import-Module PSScheduledJob; $jobDef = [Microsoft.PowerShell.ScheduledJob.ScheduledJobDefinition]::LoadFromStore('MyScheduledJob', '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\ScheduledJobs'); $jobDef.Run()"

As you can see, there is an ad-hoc command, which loads a specialized module that does the heavylifting for reading/loading and storing "job definitions" in an XML store. If you navigate to that folder, there will be one folder per ScheduledJob, with a ScheduledJobDefinition.xml which contains an XML tag called <InvocationParam_FilePath> whose value is still only the path to the file that contains the script to be executed. However, when the task is triggered, it will go and extract the contents of that file and run it as a ScriptBlock. This allows you to update the script file and the Task Scheduler will always pick up the most updated version when it runs (that's great!), while you can still trace what was exactly run each time because under the Output directory you can find a Results.xml file that contains a snapshot of the code that was actually executed (<Status_Command>).

So, as you already discovered, one simple workaround is to make it so that the script block is only a wrapper to an invocation of a script file, like in your example:

& "C:\Scriptpath\Script.ps1"
# This will effectively auto-populate 
# the $PSScriptRoot variable within the 
# boundaries of that script, making it 
# possible to be used within it.

The second workaround, if you really would like to use -Filepath option, is to use it in tandem with the -ArgumentList option to inject those values when the block gets executed. Because I register my scheduled jobs with another powershell script placed in the same location, which I interactively execute, all those auto-variables will be available at registration-time, and the values will be stored in the job definition (<InvocationParam_ArgList>) as part of its "execution context" so to speak. Now, you just need to have a way to make the script work both when called interactively and when called by the Task Scheduler. Here's an example of a backup script that I needed it to work in such way. This is how the ScheduledJob is registered:

Register-ScheduledJob -Name $taskName `
    -FilePath $pathToBackupScript `
    -ArgumentList $PSScriptRoot,$SpecialFolderDropbox `
    -RunEvery (New-TimeSpan -Days 1)

And within the script, I ensure those parameters will always have correct values (with default values, if not provided via an ArgumentList):

# BackupScript.ps1
param (
    [String] $ScriptRoot=$PSScriptRoot, 
    [String] $Dropbox=$SpecialFolderDropbox
)
#...Here goes code that can read from
#   those passed parameters
#   When run interactively, they will already
#   have auto-populated values.

#   If you want to keep using $PSScriptRoot instead 
#   of a proxy variable, you can but some linters
#   will complain/warn about this overwrite.
#   Example: $PSScriptRoot=$PSScriptRoot...
#   Just let it know who's boss :P

The third workaround is to simply go back and use good old Scheduled Tasks (in Windows). Here you can define the command to be run as you wish. Obviously, you will be invoking Powershell.exe ...<the usual switches>... -File script.ps1, and the script will properly populate the $PSScriptRoot. Personally, I haven't found any good scenario where I can assert that Scheduled Jobs are the definitive superior choice.

Solution 2:[2]

It seems like you're describing two problems:

  1. $PSScriptRoot is unpopulated under some circumstance.

  2. Set-ScheduledJob with -FilePath parameter displays the contents of the file instead of executing it.

For #1: There is an open issue filed for $PSScriptRoot but it's hard to say whether the circumstances match your use-case because... you haven't provided any code. I suggest you add a bit more source code explaining how your code uses $PSScriptRoot

For #2: The documentation for the Set-ScheduledJob : -Filepath seems to require a .ps1 file, but your example shows a non-.ps1 file being passed.

-FilePath

Specifies a script that the scheduled job runs. Enter the path to a .ps1 file on the local computer. To specify default values for the script parameters, use the ArgumentList parameter. Every scheduled job must have either a ScriptBlock or FilePath value.

Does the script at 'c:\Filepath' actually have a .ps1 extension? If not, give it one and change your -Filepath parameter to match.

It may be that one problem is leading to the other, but it's unclear from the current text in question how they are related.

Solution 3:[3]

Regarding $PSScriptRoot not being present for a ScheduledJob. We found that $PSScriptRoot not only failed for the root file executed by the scheduled job but also those subsequently included.

For example, say the root file contained . "<absolute path>\Include-SomeOtherFile.ps1" and within Include-SomeOtherFile.ps1 we then called . "$PSScriptRoot\Include-YetAnotherFile.ps1". This would fail as $PSScriptRoot is still empty.

I didn't want to hardcode everything so instead I do a check within the root file to see if $PSScriptRoot is empty. If empty it uses the call operator to start the script again but in a new context where the $PSScriptRoot works:

$ErrorActionPreference = "Stop"
 
if ($PSScriptRoot -eq "")
{   
    # Execute this file again but a new 
    # context where $PSScriptRoot works
    & "<absolute path of this file>.ps1"

    Exit $LASTEXITCODE
}
   
try 
{
    # Rest of code using $PSScriptRoot
}
catch
{
    Exit 1
}

So I still had to use the absolute path of the root file but after that I was able to rely on $PSScriptRoot throughout my included files.

According to this SO answer, if you want to calculate the absolute path and are happy relying on the name of the scheduled job, you can apparently use the following (untested):

Get-ScheduledJob |? Name -Match 'JOBNAMETAG' |% Command

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mario Vasquez
Solution 2 veefu
Solution 3 Adam Willden