Invoke-Command to set PSModulePath

Hi,

I’m calling Invoke-Command to run [Environment]::SetEnvironmentVariable(“PSModulePath”, $old + “;c:\test”, “Machine”) on a remote machine.

With a RDP session running on the same machine, starting a new PowerShell prompt, the $env:PSModulePath is not updated, instead ONLY the c:\windows\system32\windowspowershell\v1.0\modules path is shown.

If I open “control panel”, the system PSModulePath is correct. If I then save the path from “Control panel”, a new PowerShell prompt will show the $env:PSModulePath correctly. A server boot also fixes the problem.

If I run the commands inside the Invoke-Comand locally (using the rdp session) it works as expected, the path is updated when I open a new prompt.

So,

  • Why does not the PSModulePath update as expected?
  • Any suggestions on how to get the path to update as expected?
  • Are someone able to reproduce this? I have tried on several machines, but they are all similar.

The code:
Invoke-Command -ComputerName $computer -ArgumentList $localTarget,$VerbosePreference -ScriptBlock {
param(
[string]$localTarget,
[System.Management.Automation.ActionPreference] $VerbosePreference
)

$currentValue = [Environment]::GetEnvironmentVariable(“PSModulePath”, “Machine”)
if (!($currentValue.Contains($localTarget)))
{
$currentValue += “;” + $localTarget

Write-Verbose "   Adding $localTarget to PSModulePath"
[Environment]::SetEnvironmentVariable("PSModulePath", $currentValue, "Machine")

} else {
Write-Verbose " $localTarget Already in PSModulePath"
}
}

This may have something to do with how a Remoting session is spun up. You’re not actually logging on interactively; you’re sending commands to a service. If the service doesn’t have a full environment (and it doesn’t), then there may be problems changing this. I think the preferred way to make the environment variable change is GPO, right now - see http://technet.microsoft.com/en-us/library/cc772047.aspx for an example.

You can do this, but you need to consider a few things:

  1. Updating the Machine environment variable called PSModulePath does not update the PSModulePath value in the current process. It updates it on the machine. A process only sets process environment variables using machine environment variable values when it is created. That is why you don’t see the change in your current session. You need to make the update in your current process and on the machine.

  2. I would check the current process value instead of the machine value because that is what you care about most: whether or not the current process has the environment variable set.

  3. When you check this, you should split the value on semi-colon and then use the -contains or -notcontains or -in or -notin (depending on how you write the command) operator to check the value so that you’re not tripping on partial matches.

With this in mind, I would do it something like this:

Invoke-Command -ComputerName RemoteComputer -ScriptBlock {

    $requiredPSModulePathEntry = 'C:\MyModuleFolder'

    # Check the Process environment variable value first, see if it is there (this comes from Machine anyway)
    if (@($env:PSModulePath -split ';') -notcontains $requiredPSModulePathEntry) {

        # Capture the current machine environment variable value
        $oldMachineValue = [Environment]::GetEnvironmentVariable('PSModulePath','Machine')

        # Define the new machine environment variable value
        $newMachineValue = "${oldMachineValue};${requiredPSModulePathEntry}"

        # Commit the new machine environment variable value
        [Environment]::SetEnvironmentVariable('PSModulePath',$newMachineValue,'Machine')

        # Update the process environment variable value so that we can use it right away
        $env:PSModulePath = "${env:PSModulePath};${requiredPSModulePathEntry}"
    }
}

Thanks both for answering. And so fast!

First, I tried your code, Posthoholic. It leaves me with the same problem as my own, I’m afraid. It is the Machine variable I care mostly about so that further PowerShell command windows will have an updated path. If I run both yours and mine code locally it works as expected. If I restart the computer or log in and out it also work as expected. If I only starts a new console, the paths are not updated when remoting is used for setting the path. So unluckily, it seems that Don Jones is right about this.

In the end I did a work-around calling SetX.exe from my PowerShell code locally, using the -s parameter to update a remote computer. That worked as expected.

Posthoholic, thanks for your better check. I have updated my code accordingly. I really liked the readability and compactness of that more correct but also more complex test.

SetX is a good tool. If you decide to try some other options without relying on the external program, here are a couple of ideas:

  • Try setting the value directly in the registry, and see if that works better than calling [Environment]::SetEnvironmentVariable via a remote session. The system environment variables are stored in "HKLM:\System\CurrentControlSet\Control\Session Manager\Environment". The command to update the value with the $newMachineValue variable (in Poshoholic's last post) would be something like Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name 'Path' -Type ExpandString -Value $newMachineValue
  • If that doesn't work, you can also try sending a WM_SETTINGCHANGE broadcast message to existing processes, which is the part that's probably failing if you say the new value doesn't take effect until a reboot. This requires a little bit of P/Invoke code to access a Win32 API function (code listed below).
Add-Type -TypeDefinition @"
    using System;
    using System.Runtime.InteropServices;

    public class NativeMethods
    {
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessageTimeout(
            IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,
            uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
    }
"@

$HWND_BROADCAST = [IntPtr] 0xffff
$WM_SETTINGCHANGE = 0x1a
$SMTO_ABORTIFHUNG = 0x2
$result = [UIntPtr]::Zero

[void] ([Nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, 'Environment', $SMTO_ABORTIFHUNG, 5000, [ref] $result))