|23 Nov 2009 Update - TimeZoneLib.ps1 in the download has been updated to allow proper redirection of the script output to a file.|
A few year back a customer of mine was setting the time zone on their computers by directly modifying registry entries. Since the supported way to do this was to use the SetTimeZoneInformation Win32 API, I created a utility in Visual Basic 6 to allow the customer to set the time zone on their Windows computers from the command line.
Later on, Windows XP included a built-in method to do this using control.exe with a command like:
Control.exe TIMEDATE.CPL,,/Z Pacific Standard Time
where the value after /Z is from the Std or Display values under the time zone subkey of the registry key HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones.
Unfortunately, the control.exe method no longer works on Windows Vista. When I tried my old VB6 utility on Vista I discovered it no longer worked either. After asking about it on an internal discussion group, I was informed that my old utility did not work for several reasons.
First, a new privilege (SeTimeZonePrivilege) was created for Windows Vista so that standard users could now change the time zone. The Date and Time control panel enables this privilege when you use it to change the time zone. However, custom programs have to enable this privilege programmatically. Using the AdjustTokenPrivileges Win32 API is one way to do this. The second reason was that Windows Vista has a new API to set the time zone – SetDynamicTimeZoneInformation. This API allows control over whether the system uses Dynamic Daylight Savings Time (daylight savings transition dates that change from year to year). The old API still works but will set the Dynamic Daylight Savings Time setting incorrectly. It is also recommended that the time zone name parameters for SetDynamicTimeZoneInformation are set from the MUI_Std and MUI_Dlt values from the time zone Registry subkey. To translate these values into the proper language specific string requires using either the RegLoadMUIString or SHLoadIndirectString API.
So I updated my VB6 utility and it now works fine on Windows Vista and higher. I thought about sharing this version publically but then I decided it would be better if I could demonstrate doing this with a modern language, preferably a scripting language. Since the Windows Script Host has no mechanism for using Windows API calls, I could not use that. Luckily there is a newer Windows scripting language that is capable of doing this (albeit indirectly). That language is Windows PowerShell.
PowerShell itself cannot call Win32 APIs directly. However, it can use the power of the .NET Framework to do this. PowerShell can use .NET objects directly using the new-object cmdlet as show here. However, to call Win32 APIs you have to write some C# code and either create a cmdlet or compile it on the fly from the script. Since I did not want to have to compiled assemblies as part of this solution I chose the latter. In PowerShell v1 this can be done with functions like Compile-Csharp found here. (Note that in the upcoming v2 release there is an Add-Type cmdlet for this.) Using PINVOKE (Platform Invoke) to call Win32 APIs in C# can be tricky so I’ve provided a few references in case you want to try this for other Windows APIs:
The attached script files implement two commands. Set-TimeZone is for setting the current time zone. Get-TimeZone to get the current time zone information and to list the information for all time zones defined in the Registry. All five files in the attached Zip need to be in the same folder. Run Set-TimeZone and Get-TimeZone with the –help switch for usage.
If you want to create similar scripts here are a few tips from what I learn doing this. First, PowerShell v1 does not like it when you reference the C# classes, structures, etc. in the same script that compiles the code. I originally had a single library script that contained the here-string with the C# code, the step to compile it and the functions that used it. This worked fine on v2 but v1 gave me errors that it could not find the C# classes, structures, etc referenced in the functions. So to get this to work on v1 I had to put the here-string in a separate script (TimeZoneCSharp.ps1) from the functions that use it (TimeZoneLib.ps1). Then in Get-TimeZone and Set-TimeZone I dot sourced TimeZoneCSharp.ps1 and compiled the C# first, then dot sourced TimeZoneLib.ps1.
Second, if you use the Compile-Csharp function found here you need to comment out the $cpar.OutputAssembly = "custom" line. This line is not needed since we only want to compile the code in memory. Also, this line causes the compiler to try to write out an assembly in the Windows\System32 folder. This makes the script fail when run as as standard user.
Note that Windows 7 now includes a command line utility to set the time zone and get time zone information – tzutil.exe.
This post was contributed by Michael Murgolo, a Senior Consultant with Microsoft Services - U.S. East Region.