15

I'm writing a Win32 DLL with a function that adds a directory to the Windows PATH environment variable (to be used in an installer).

Looking at the environment variables in Regedit or the Control Panel after the DLL has run shows me that my DLL has succeeded in adding the path to HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment and HKEY_CURRENT_USER\Environment.

But when I start up a new Command Prompt (after running the DLL), the directory I added does not show up in the output of echo %PATH% and I can not access the executable that lives in that directory by typing its name.

I think my program is not doing a good job of notifying the system that the PATH has changed, or maybe it is notifying them before the change has fully taken effect. I read an article by Microsoft that says to broadcast the WM_SETTINGCHANGE message after changing an environment variable, and I am doing that with this code:

DWORD result2 = 0;
LRESULT result = SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
    (LPARAM)"Environment", SMTO_ABORTIFHUNG, 5000, &result2);
if (result == 0){ /* ... Display error message to user ... */ }

The order of my calls is: RegCreateKeyEx, RegSetValueEx, RegCloseKey, SendMessageTimeout

If I press "OK" in the Control Panel "Environment Variables" window, the changes made by my DLL to the PATH show up in newly-created command prompts, so there is something that the Control Panel is doing to propagate PATH changes; I want to figure out what it is and do the same thing.

Does anyone know what I should do?

I'm running 64-bit Windows Vista but I want this to work on all Windows XP, Vista and Windows 7 operating systems.

Update: The problem with the code I posted above is that I did not put the L prefix on the "Environment" string. Although it does not say it explicitly anywhere in the Microsoft documentation that I can find, the LPARAM needs to be a pointer to a WCHAR string (2-byte characters) as opposed to a CHAR string, which is what Visual Studio's compiler generates by default when I write a string literal. The solution to my problem was to change "Environment" to L"Environment". (I thought I already tried that before posting this question, but apparently I didn't try it correctly!) But anyone who wants a complete C++ solution for this task should look at Dan Moulding's answer.

Julien Roncaglia
  • 17,397
  • 4
  • 57
  • 75
David Grayson
  • 84,103
  • 24
  • 152
  • 189
  • You may want to look at the source code of some open-source installers, e.g. NSIS or Inno Setup. They do this notification right. – bialix Dec 17 '09 at 02:54
  • 7
    One tiny quick tip, you can show the PATH contents at the command prompt by just typing "path" and pressing enter. Saves you 7 keystrokes per test (9 counting the shift key)! – Todd Dec 17 '09 at 03:05
  • Ah, you must be building a Unicode application, so `windows.h` brings in the Unicode version of the API (`SendMessageTimeoutW`), which would require a `LPCWSTR` for the `LPARAM` instead of `LPCSTR`. I think if you want to support both "ANSI" and Unicode builds, then the right thing to do is to use `LPCTSTR` (i.e. via the `_T()` macro). My library could probably use an update to make it "Unicode aware". It currently only supports setting environment variables using "ANSI", which could be problematic if you need a directory with, say, cyrillic characters, in your path. – Dan Moulding Dec 17 '09 at 19:14
  • I doubt that SendMessageTimeout[W] would ever convert the LPARAM from one string type to another, because as far as I know lparam is just a general pointer which could point to anything. So when you generate the string you have to make sure it is in Unicode format before giving it to SendMessageTimeOut[W]. I'm using Visual Studio 2008, including windows.h, and not doing anything special with regards to Unicode. In my program literal strings like "Environment" are ANSI by default (which caused my problem). What compiler are you using, and what encoding ares literal strings by default? – David Grayson Dec 17 '09 at 19:50
  • Right, there are two versions of the function: `SendMessageTimeoutA` and `SendMessageTimeoutW`. The "A" version's LPARAM is expected to be a LPCSTR, while the "W" version's LPARAM is expected to be a LPCWSTR. If you call the function by just `SendMessageTimeout` then you really should enclose the LPARAM in the `_T()` macro, i.e.: `_T("Environment")`. Then, if windows.h expands `SendMessageTimeout` as `SendMessageTimeoutW`, it will also expand `_T("Environment")` as `L"Environment"`. Otherwise (for non-Unicode) they are expanded as `SendMessageTimeoutA` and `"Environment"`, respectively. – Dan Moulding Dec 18 '09 at 02:20
  • I guess David was writing a Custom Action DLL to his setup project when he posted this question. I'm doing the same thing now, and wondering if there exists a simpler solution to add to PATH?? For example, might I be able to specify somewhere a script to run after the installation so I don't have to code?? – yaobin May 22 '13 at 11:33
  • @rob, this might not be too helpful to you, but I would recommend moving away from Visual Studio setup projects and using WiX instead. WiX is *free* (whereas setup projects are not), it gives you a better understanding of what is going on under the hood, and it has built-in support for adding directories to the path ( http://stackoverflow.com/questions/1931586/can-anyone-give-me-a-example-of-modifying-windows-environment-system-variables-i ). The only downside is that you have to write a tricky XML file and generate some GUIDs. – David Grayson May 22 '13 at 16:55
  • @DavidGrayson: Nice! Visual Studio is not the ONLY option for me so I'll go and give WiX a try. Thanks for the recommendation! – yaobin May 23 '13 at 01:21
  • possible duplicate of [Is there a way to set the environment path programatically in C++ on Windows?](http://stackoverflow.com/questions/531998/is-there-a-way-to-set-the-environment-path-programatically-in-c-on-windows) – user Apr 01 '14 at 19:13

2 Answers2

12

It turns out there really isn't anything new under the sun. This has already been done before, at least once. By me. I created a DLL very much like what you describe for exactly the same purpose (for use in modifying the path from an NSIS installer). It gets used by the Visual Leak Detector installer.

The DLL is called editenv.dll. The source is available at github. I just tested the installer and it updated the system PATH environment variable, no problem. Based on what you've written, I don't see anything that stands out as being wrong. I also don't see anything obvious that's missing. But it may be worth a look at the editenv.dll source (you'd be most interested in EnvVar::set() in EnvVar.cpp, and possibly the pathAdd() and pathRemove() C APIs in editenv.cpp).

Dan Moulding
  • 211,373
  • 23
  • 97
  • 98
  • Thanks for the code Dan! Even thought I could not find my specific solution by looking at your code, your answer will be immensely helpful to future readers of this question. – David Grayson Dec 17 '09 at 18:25
0

I have a program which calls the same Win32 API to yours to update the environment, and it works fine.

One thing to be careful of is how you are opening up the command prompt.

If you open up the command prompt by doing this:

Start -> Run -> cmd.exe

then the environment in the prompt shows that the new variable is set.

However, I also have a programmable function key on my keyboard which I have set to run the cmd.exe process. If I open a command prompt via that function key and then type env, it doesn't show the variable as being set.

I'm not sure why it works differently, but it must have something to do with the way the cmd.exe process is launched (although both are running under my user name, not SYSTEM).

How are you opening up the command prompt?

LeopardSkinPillBoxHat
  • 28,915
  • 15
  • 75
  • 111
  • 4
    The program that manages your programmable function keys is starting CMD.EXE with its own copy of the environment, which hasn't been updated if you didn't restart it after changing the path. – Todd Dec 17 '09 at 03:12
  • Interesting. I was starting my command prompt by clicking on "Command Prompt" in my start menu. This is a shortcut that came with Windows, in the Accessories menu, which I "pinned" to the start menu. The target is %SystemRoot%\system32\cmd.exe – David Grayson Dec 17 '09 at 03:29
  • @Todd - yep that sounds right. Good analysis! @David - Any difference if you run cmd.exe from Start->Run? – LeopardSkinPillBoxHat Dec 17 '09 at 03:39
  • 2
    On my system, running it from the start menu shortcut versus using Start->Run makes no difference. Which makes sense, because the main "shell" (explorer.exe) updates its environment as a result of the WM_SETTINGCHANGE broadcast. Any processes it spawns (whether via Start->Run or otherwise) should inherit that updated environment. – Dan Moulding Dec 17 '09 at 03:57
  • @LeopardSkinPillBoxHat, I don't know if there was a difference; I already solved the problem though and it works fine now. Thanks for the suggestion. – David Grayson Dec 17 '09 at 19:49