Tuesday, April 9, 2013

Creating Custom Windows Timezones

I spend a lot of time dealing with time zones. I generally use one of two approaches:

  1. Store an offset
  2. Store a Windows Time Zone identifier

Method one is quick and straight-forward, but not very flexible: it doesn't account for daylight savings, for a start. Method two is more complex, but generally more powerful - .Net gives you some great tools for working with time zones, and has some basic safety checks which (sometimes) prevent you from doing silly things like doubling up your time zone conversions.

I recently dealt with an interesting issue: a non-standard time zone.

We retrieve data from lots of remote monitoring equipment. We talk to a variety of different types of gear, most of which are designed to be simple and reliable, and operate using as little power as possible. In practice, this means the equipment uses a simple clock, with no time zone awareness. Industry practice (which I'm often stuck with) is to configure the equipment with the local time (which violates rule 1: Do everything in UTC.)

This particular piece of equipment had been deployed during the summer, while daylight saving was in effect, and the clock had been set to the local daylight time: UTC+10.5 (yes, there are time zones on the half hour!). When we configured the data connector at the server end, we spotted a handy time zone with the right offset (Adelaide) and set it to use that. Of course, nobody thought about daylight saving, and so when it ended our Adelaide time zone suddenly reverted to UTC+9.5, and so all of the incoming data started being treated as if it were UTC+9.5 instead of UTC+10.5 - which was a problem.

Of course, we've dealt with this sort of problem before: most of our equipment has no daylight saving support, but lots of our customers are in places which follow daylight saving. We normally just find a time zone with the correct offset but no DST: for example, gear in Sydney gets set to either +10 (and we use the Brisbane time zone) or +11 (and we use somewhere like Port Vila, which is +11 year-round). However, when we went to find somewhere which was on UTC+10.5 year round, we ran into a problem. There is no such place.

We didn't really want to make code changes to support this situation, but we had to do something, and it didn't take much digging to discover that .Net pulls its time zone information from the Windows registry. After a quick look at the relevant keys, however, it quickly became obvious that it wasn't going to be simple to craft a custom entry. The Adelaide key looks like this:
The first six entries are fine. The three values with names starting with 'MUI' are just localisation references, and because we're just doing this on one of our servers, we don't care about that - and it turns out that you can ignore the whole @dll syntax and just put a string in here. Great!

That TZI value looks nasty - and it turns out that it is. Fortunately, Microsoft provides an editor to modify these entries. Unfortunately, it was buried so thoroughly that we didn't find it at the time (nor did StackOverflow!) and so we pushed on with our research. We found our answers in the MSDN TIME_ZONE_INFORMATION structure page. Despite the fact that the whole point of the registry is to provide convenient key/value pairs, Microsoft decided to go with storing a C struct in hex values. The TZI field is a hex dump of the _REG_TZI_FORMAT struct:

typedef struct _REG_TZI_FORMAT
    LONG Bias;
    LONG StandardBias;
    LONG DaylightBias;
    SYSTEMTIME StandardDate;
    SYSTEMTIME DaylightDate;

Two things immediately stood out. c6 fd ff ff was not +9.5, nor was it +570 (if it were stored in minutes). It turns out the registry stores the negative of the time zone offset in minutes - if you fire up your programmer's calculator, you'll see that 0xfffffdc6 is, in fact, -570. This isn't the only quirk - the date fields have some pretty specific requirements, the DaylightBias is in fact the difference between the daylight total offset and the Bias field, and the StandardBias is an optional offset from the Bias field to the standard (non-DST) time, which is generally (always?) set to 0.

Because I don't like to half-do things (and I spotted a chance to brush off my rather dusty C++ skills) I built a tool to accept the required fields and churn out a file ready to be imported directly into the registry (keep in mind at this stage we hadn't found TZEdit). It's unlikely to ever be polished, but it's available on GitHub.

I don't really know what to make of this experience. I could have saved myself some work by continuing to search for a ready-made time zone editor, but the clock was ticking to get this fixed, and I found enough information to solve the immediate problem in much less time than it took me to find the editor (I didn't build the registry file generator until after we had hand-crafted the registry entries we needed).

I think the real lesson to take out of this is that storing binary data like the TZI field destroys the usability of the registry. If Microsoft had used a few sensibly-named fields instead of dumping cryptically-formatted binary data into a single field, we could have solved the problem with much less effort.

Keep this in mind when building your own configuration systems!

No comments:

Post a Comment