Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

When Can I Logon to Windows?

4.33/5 (5 votes)
18 Sep 2010CPOL9 min read 22.1K   183  
A brief explanation on how to interpret the 'logon hours' member of the USER_INFO structures

Introduction

On my Windows 7 machine, I happened upon the Time Limits dialog used by Parental Controls.

Image 1

This intrigued me enough to wonder how this information might be obtained. I quickly found the NetUserGetInfo() function. This function populates one of nine different USER_INFO_x structures with various information about a particular user account on a server. However, each time I found an example of what the members of the structure looked like, they all treated the usriX_logon_hours member the same: as 21 separate bytes. Printing the other structure members formatted with %s or %d is fine, but I found nothing useful from looking at that member formatted with %x. Thus, my mission began...

Reading the Array

MSDN tells us that the usriX_logon_hours member is a 168-bit array (laid out from 0 to 167) with each bit representing an hour of the day. I set one of the user accounts on my computer to have the following allowed times: Sun 13-20; Mon-Thu, Sat 9-20; Fri 9-21. If you laid this array out so that it looked like a typical 24-hour week, you'd have the following (with the alternating shades of gray showing the byte boundaries).

0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
Sun000000000000011111110000
Mon000000000111111111110000
Tue000000000111111111110000
Wed000000000111111111110000
Thu000000000111111111110000
Fri000000000111111111111000
Sat000000000111111111110000

So, how to read the 21-byte array so that it looks something like the above? Reading from the array is simple enough, but we're dealing with bits not bytes. I found it easier to look at the array as a 1x168 bit array rather than a 1x21 byte or a 7x24 bit array. The first order of business, then, will be to "convert" the 21 bytes into 168 bits. I did this with something that looks like:

C++
LPBYTE lpLogonHours = lpUserInfo->usri2_logon_hours;
int nBits[7 * 24];

for (int x = 0; x < 21; x++)
{
    // get bit 8, bit 7, bit 6, etc
    for (int y = 7; y >= 0; y--)
        nBits[z++] = (*lpLogonHours >> y) & 0x01;

    lpLogonHours++; 
}

This produces a 168-bit array, arranged in a familiar 7x24 table, that looks like:

0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00
Sun000000110000000011111000
Mon000000111000000011111111
Tue000000111000000011111111
Wed000000111000000011111111
Thu000000111000000011111111
Fri000000111000000011111111
Sat000001111000000011111111

Hmm, this does not look quite like what we are after. The bits appear to have some sense of uniformity, but some things don't quite line up. For example, I would have expected Sunday to contain seven consecutive 1s for the 13:00-20:00 hour slots.

MSDN tells us that the bits must be adjusted (i.e., shifted) according to our time zone. With UTC±0, for example, the first bit (of the first byte) is Sunday, 0:00 to 0:59; the second bit is Sunday, 1:00 to 1:59; and so on like above. Ok, but since I am in the UTC-6 timezone, the starting bit for me would be 6 (of the first byte). That would be my Sunday from 0:00 to 0:59; my Sunday from 1:00 to 1:59 would be bit 7; and so on. Also, since my Sunday started with bit 6, bits 0-5 are the last 6 hours of Saturday (this "wrapping" will become more apparent). The first 24 hours now look like:

Sat 18:0019:0020:0021:0022:0023:00Sun 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
000000110000000011111000

It changed, but does not really look any better. Let's see if there's anything else that needs tweaking.

Reading Each Byte in Reverse Order

Looking at the first table, I should see a 1 in the 18:00-19:00 slots for Saturday, but I'm seeing a 0 instead. I do, however, see two 1s at the other end of that byte. Also, I should see 1s in the 13:00-17:00 slots for Sunday (the last five hours), but I'm seeing them in the 10:00-14:00 slots instead (the first five hours). In both cases, it appears that the bits are backwards for each byte. When converting the bytes to bits, it seems maybe we should simply iterate the bits in the opposite order, like:

C++
LPBYTE lpLogonHours = lpUserInfo->usri2_logon_hours;
int nBits[7 * 24];

for (int x = 0; x < 21; x++)
{
    // get bit 1, bit 2, bit 3, etc
    for (int y = 0; y < 8; y++)
        nBits[z++] = (*lpLogonHours >> y) & 0x01;

    lpLogonHours++; 
}

The table now looks like the following. Notice how since the Sunday 0:00-0:59 hour has been offset by 6 hours from the beginning of the array, all subsequent hours have been offset as well, with the last 6 hours of Saturday being wrapped back around to the beginning of the array.

Sat 18:0019:0020:0021:0022:0023:00Sun 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
110000000000000000011111

Sun 18:0019:0020:0021:0022:0023:00Mon 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
110000000000000111111111


Mon 18:0019:0020:0021:0022:0023:00Tue 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
110000000000000111111111

Tue 18:0019:0020:0021:0022:0023:00Wed 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
110000000000000111111111

Wed 18:0019:0020:0021:0022:0023:00Thu 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
110000000000000111111111

Thu 18:0019:0020:0021:0022:0023:00Fri 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
110000000000000111111111

Fri 18:0019:0020:0021:0022:0023:00Sat 0:001:002:003:004:005:006:007:008:009:0010:0011:0012:0013:0014:0015:0016:0017:00
111000000000000111111111

Much better!

Find the Right Starting Point in the Array

When we go to create the table at the top of this article, it's simply a matter of finding the right bit in the array to start reading from. By looking at each of the timezones and starting offsets in this table, we can see where we should start reading from, and we also might see a pattern:

If TZ is...Then array offset is...
-1212
-1111
-1010
-99
-88
-77
-66
-55
-44
-33
-22
-11
00
1167
2166
3165
4164
5163
6162
7161
8160
9159
10158
11157
12156
13155

The way of achieving the starting offset first requires us to know how many hours there are between Coordinated Universal Time (UTC) and local time.

C++
TIME_ZONE_INFORMATION tzi;
GetTimeZoneInformation(&tzi);

int nOffset = tzi.Bias / -60;

Second, we then adjust that value forward or backward, like:

C++
nOffset = (168 - nOffset) % 168;

Now that we know where to start reading the array from, we can simply iterate through all 168 bits. But what happens when we get to the end of the array but haven't read all 168 bits yet? Answer: Just wrap back around to 0. One way to do this is:

C++
nOffset = nOffset + 1;
if (168 == nOffset)
    nOffset = 0;

Or if you're into brevity, a one-line solution would be:

C++
nOffset = (nOffset + 1) % 168;

Epilogue

While I used hard-coded values like 24 and 168 in the code snippets above, it was just to make the text easier to read. In the accompanying sample, however, I used the #define macros found in the lmaccess.h file.

It was not my intention with this article to make a replacement for the Time Limits dialog used by Vista and Windows 7, nor was I interested in creating a full-blown "restriction" application. Several of those already exist. I simply wanted to show how to read the information. I leave "write" capabilities to the interested reader.

Enjoy!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)