Search code examples
powershellwindows-subsystem-for-linuxwindows-task-scheduler

Failure to run WSL internal command from powershell


I need to run WSL internal command from powershell script triggered by task scheduler on user logon. OS: Windows 11 22H2.

# waiting for wsl.exe to appear
# before adding this I was getting errors about no command wsl
# probably something about how MSIX packages work in Windows
while (-Not (Get-Command wsl.exe))
{
    Start-Sleep -Milliseconds 500
}

while (-Not (wsl.exe ip a))
{
    Write-Output $LastExitCode
    Start-Sleep -Milliseconds 500
}

Result in script logs is:

1
1
1
...

After logon the script runs infinitely. WSL commands are available, I can write in a console wsl.exe ip a and get the result and $LastExitCode is 0. But the script is still running until I stop the task in task scheduler. Somehow the script can not get access to WSL.

When I run the script from admin console, it works fine.

Task options in task scheduler:

  • When running the task, use the following user account: <my account>
  • (*) Run only when the user is logged on
  • [x] Run with highest privileges
  • Triggers: at log on: <my account>
  • Actions: Start a program: powershell.exe -file "<path to the script>"

"Run with highest privileges" is necessary because the script needs to run netsh commands.

How can I fix it to run WSL command?

In Windows 10 the same script with the same task scheduler settings works fine.


Solution

  • This appears to be a known problem as of this writing - see GitHub issue #9231 - and it affects the following scenario:

    • Your WSL version was installed from the Microsoft Store (either via the application or via winget.exe).

    • You're trying to launch wsl.exe from session 0, i.e. the hidden services window station in which services run.

      • This, in turn, implies that you've chosen a logon method that involves a service, which applies whenever the Run whether user is logged or not option in the Task Scheduler GUI (taskschd.msc) is selected (which also applies when you run in the context of a privileged system account such as NT AUTHORITY\SYSTEM).[1]

    You've found the workaround yourself:

    • Run your task with the Run only when user is logged on option selected, for which you have two options:

      • Configure your run-at-logon task to run as a specific user, i.e. only when that user logs on.
      • Configure your run-at-logon task to run whenever a user from a given group logs on, say Users (to target all users).
        Note that this assumes that all accounts in the group have at least one WSL distro installed in the context of their account.
    • The workaround is effective whether or not Run with highest privileges is checked.

    • The price of this workaround is that your task invariably runs visibly at logon (a console window will open that auto-closes when the task exits).


    With the workaround in place, my informal tests suggest that you do not need to wait for the wsl.exe executable to become available in your script (and even the first wsl.exe ip a call succeeds); to be safe, a loop - albeit just a single one - is retained in the following reformulation of your script:

    do {
      # Note: 
      # * To allow the logs to capture *stderr* output too, add 2>&1  
      # * The `catch` block triggers only if `wsl.exe` can't be found.
      #   Exit code `2` is used to signal that fact, but you can set any nonzero one.
      try { wsl.exe ip a } catch { $global:LASTEXITCODE = 2 }
      if ($LASTEXITCODE -eq 0) { break }
      $LASTEXITCODE   # Output the nonzero exit code.
      Start-Sleep -Milliseconds 500
    } while ($true)
    

    Note:

    • You should not apply the -not operator / direct to-Boolean coercion to external-program calls in an effort to test their process exit code:

      • In PowerShell, doing so coerces a command's output to a Boolean ($true or $false), which in the case of external programs applies to their stdout output, and only the absence of stdout output is coerced to $false (which -not inverts to $true).

      • However, you cannot infer the success status of an external-program call from the presence or absence of stdout output: there may be such output even if the process exit code ultimately signals failure (nonzero value) and, conversely, a process may succeed (0) quietly (without stdout output):

        • Case in point: while (-not (wsl.exe true)) { } is by definition an infinite loop, because the true shell builtin only sets an exit code, without producing stdout output, so that -not (wsl.exe true) is always $true (it is in effect the same as -not $null).
    • The only reliable way to determine success vs. failure of an external-program call is to examine its process exit code, which in PowerShell requires the automatic $LASTEXITCODE variable, as shown above.


    [1] It seems that it isn't strictly about session 0 per se, because running with the following built-in system accounts, which also run in this session, doesn't fail, but complains about no distros being installed in the context of these accounts: LOCAL SERVICE and NETWORK SERVICE. In other cases you get errors: Trying to run as a a specific user with Run whether user is logged or not selected, results in error The file cannot be accessed by the system; trying to run with the built-in SYSTEM account results in error Access denied.