Thursday, June 05, 2008

Setting expandable environment variable in .NET

The System.Environment class in .NET provides methods that allow you to set and retrieve environment variables via the

Environment.SetEnvironmentVariable(variableName, value, environmentVariableTarget) and Environment.GetEnvironmentVariable(variableName)  methods.

You can use Environment.GetEnvironmentVariable to get the values stored in expandable path variables such as %PROGRAMFILES%, %WINDIR%. (For more variables see: Environment Variables in Windows XP).

But .NET doesn't provide you with a way to set an expandable variable as an environment variable.

A little background behind expandable variables is in order. When expandable variables are stored in the registry, they are stored in a special registry data type called "REG_EXPAND_SZ". This is used to signal that the value held in the registry requires to be expanded and not read literally.

If you call Environment.SetEnvironmentVariable("NewExpandedPATH", "%WinDir%, EnvironmentVariableTarget.Machine) and then call Environment.GetEnvironmentVariable("NewExpandedPATH"); the value returned is null.

The reason is that SetEnvironmentVariable creates the NewExpandedPath value in the registry as a REG_SZ data type instead of REG_EXPAND_SZ. For some reason the BCL team decided to leave this feature out.

To get around this issue - there are many different solutions available. One involves creating and writing REG_EXPAND_SZ data types using PInvoke. (see PInvoke.net ADVAPI32.dll - http://www.pinvoke.net/default.aspx/advapi32.RegSetValueEx)

Instead the solution I liked best was the one written by Greg Houston. It involves using Windows Scripting Host (WSH) to write the value to the environment. In addition, it also prompts the system to refresh the environment variables via the call to SendMessageTimeOut. This second feature is extremely important - as the environment variable becomes available immediately after it is set to the same process. (This would not happen if we decided to write directly to the registry - which would need a system restart). Read more about this solution on Greg' site (Greg Houston: How to Create and Change Environment Variables using C# or .Net).

An important piece about this solution that I think he left out is that you need to add a reference to the COM dll (wshom.ocx - Windows Script Host Object Model on the COM tab of the Add Reference dialog).

Here is the solution repeated from Greg's site:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using IWshRuntimeLibrary; //add reference to wshom.ocx - Windows Script Host Object on the COM tab
class AdvancedRegistryMethods
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SendMessageTimeout(IntPtr hWnd, int Msg, int wParam, string lParam, int fuFlags, int uTimeout, out int lpdwResult);
        public const int HWND_BROADCAST = 0xffff;
        public const int WM_SETTINGCHANGE = 0x001A;
        public const int SMTO_NORMAL = 0x0000;
        public const int SMTO_BLOCK = 0x0001;
        public const int SMTO_ABORTIFHUNG = 0x0002;
        public const int SMTO_NOTIMEOUTIFNOTHUNG = 0x0008;

        public static bool SetUserVariable(string name, string value, bool isRegExpandSz) 
        { 
           return SetVariable("HKEY_CURRENT_USER\\Environment\\" + name, value, isRegExpandSz); 
        }        
        
        public static bool SetSystemVariable(string name, string value, bool isRegExpandSz) 
        {
            return SetVariable("HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\" + name, value, isRegExpandSz);
        }
        /// <summary>
        /// return false if error occurs
        /// </summary>
        /// <param name="fullpath"></param>
        /// <param name="value"></param>
        /// <param name="isRegExpandSz">if true - written to regsitry as a REG_EXPAND_SZ</param>
        /// <returns></returns>
        private static bool SetVariable(string fullpath, string value, bool isRegExpandSz)
        {
            try
            {
                object objValue = value;
                object objType = (isRegExpandSz) ? "REG_EXPAND_SZ" : "REG_SZ";
                WshShell shell = new WshShell();
                shell.RegWrite(fullpath, ref objValue, ref objType);
                int result;
                SendMessageTimeout((System.IntPtr)HWND_BROADCAST, WM_SETTINGCHANGE, 0, "Environment", SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG, 5000, out result);
                return true;
            }
            catch (Exception exp)
            {
                SetEnvironment.WL(exp.Message);
                return false;
            }
        }
    }
Note: The environment variables are stored under this path in the registry:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment

No comments: