April, 2005

  • The Old New Thing

    What is the DC brush good for?


    The DC brush GetStockObject(DC_BRUSH) is a stock brush associated with the device context. Like the system color brushes, the color of the DC brush changes dynamically, but whereas the system color brushes change color based on the system colors, the color of the DC brush changes at your command.

    The DC brush is handy when you need a solid color brush for a very short time, since it always exists and doesn't need to be created or destroyed. Normally, you have to create a solid color brush, draw with it, then destroy it. With the DC brush, you set its color and start drawing. But it works only for a short time, because the moment somebody else calls the SetDCBrushColor function on your DC, the DC brush color will be overwritten. In practice, this means that the DC brush color is not trustworthy once you relinquish control to other code. (Note, however, that each DC has its own DC brush color, so you need only worry about somebody on another thread messing with your DC simultaneously, which doesn't happen under any of the painting models I am familiar with.)

    The DC brush is amazingly useful when handling the various WM_CTLCOLOR messages. These messages require you to return a brush that will be used to draw the control background. If you need a solid color brush, this usually means creating the solid color brush and caching it for the lifetime of the window, then destroying it when the window is destroyed. (Some people cache the brush in a static variable, which works great until somebody creates two copies of the dialog/window. Then you get a big mess.)

    Let's use the DC brush to customize the colors of a static control. The program is not interesting as a program; it's just an illustration of one way you can use the DC brush.

    Start, as always, with our scratch program, and making the following changes.

    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
      g_hwndChild = CreateWindow(TEXT("static"), NULL,
            WS_VISIBLE | WS_CHILD, 0, 0, 0, 0,
            hwnd, NULL, g_hinst, 0);
     if (!g_hwndChild) return FALSE;
     return TRUE;
    HBRUSH OnCtlColor(HWND hwnd, HDC hdc, HWND hwndChild, int type)
      FORWARD_WM_CTLCOLORSTATIC(hwnd, hdc, hwndChild, DefWindowProc);
      SetDCBrushColor(hdc, RGB(255,0,0));
      return GetStockBrush(DC_BRUSH);
        HANDLE_MSG(hwnd, WM_CTLCOLORSTATIC, OnCtlColor);

    Run this program and observe that we changed the background color of the static window to red.

    The work happens inside the OnCtlColor function. When asked to customize the colors, we first forward the message to the DefWindowProc function so that the default foreground and background text colors are set. (Not relevant here since we draw no text, but a good thing to do on principle.) Since we want to override the background brush color, we set the DC brush color to red and then return the DC brush as our desired background brush.

    The static control then takes the brush we returned (the DC brush) and uses it to draw the background, which draws in red because that's the color we set it to.

    Normally, when customizing the background brush, we have to create a brush, return it from the WM_CTLCOLORSTATIC message, then destroy it when the parent window is destroyed. But by using the DC brush, we avoided having to do all that bookkeeping.

    There is also a DC pen GetStockObject(DC_PEN) which behaves in an entirely analogous manner.

  • The Old New Thing

    When people ask for security holes as features: Hiding files from Explorer


    By default, Explorer does not show files that have the FILE_ATTRIBUTE_HIDDEN flag, since somebody went out of their way to hide those files from view.

    You can, of course, ask that such files be shown anyway by going to Folder Options and selecting "Show hidden files and folders". This shows files and folders even if they are marked as FILE_ATTRIBUTE_HIDDEN.

    On the other hand, files that are marked as both FILE_ATTRIBUTE_HIDDEN and FILE_ATTRIBUTE_SYSTEM remain hidden from view. These are typically files that involved in the plumbing of the operating system, messing with which can cause various types of "excitement". Files like the page file, folder configuration files, and the System Volume Information folder.

    If you want to see those files, too, then you can uncheck "Hide protected operating system files".

    Let's look at how far this game of hide/show ping-pong has gone:

    1.Normal file
    2.Hidden file
    3."Show hidden files"
    4.Hidden + System
    5."Show protected
    operating system files"

    You'd think this would be the end of the hide/show arms race, but apparently some people want to add a sixth level and make something invisible to Explorer, overriding the five existing levels.

    At some point this back-and-forth has to stop, and for now, it has stopped at level five. Adding just a sixth level would create a security hole, because it would allow a file to hide from the user. As a matter of security, a sufficiently-privileged user must always have a way of seeing what is there or at least know that there is something there that can't be seen. Nothing can be undetectably invisible.

    If you add a sixth level that lets a file hide from level five, then there must be a level seven that reveals it.

  • The Old New Thing

    Project update: Voyage to Our Hollow Earth


    In December 2003, I reported on Steve Currey's expedition to the hole at the top of the earth, which at the time was scheduled for June 26, 2005. But on May 6, 2004, the site rescheduled the trip for Jun 26, 2006 with no explanation. The reservation form reminds you that the 25% deposit is non-refundable.

    Far be it from me to suggest that these people are just stringing their loyal following along, pocketing the $4000 deposit, with no intention of actually mounting the expedition. That would be patently unfair of me. I'm certain there's a perfectly reasonable and honorable explanation for the delay.

  • The Old New Thing

    What is the HINSTANCE passed to CreateWindow and RegisterClass used for?


    One of the less-understood parameters to the CreateWindow function and the RegisterClass function is the HINSTANCE (either passed as a parameter or as part of the WNDCLASS structure).

    The window class name is not sufficient to identify the class uniquely. Each process has its own window class list, and each entry in the window class list consists of an instance handle and a class name. For example, here's what the window class list might look like if a program has two DLLs, both of which register a class name "MyClass", passing the DLL's handle as the HINSTANCE.

    HINSTANCEClass name

    When it comes time to create a window, each module then passes its own HINSTANCE when creating the window, and the window manager uses the combination of the instance handle and the class name to look up the class.

    CreateWindow("MyClass", ..., hinstA, ...); // creates class 6
    CreateWindow("MyClass", ..., hinstB, ...); // creates class 7
    CreateWindow("MyClass", ..., hinstC, ...); // fails

    This is why it is okay if multiple DLLs all register a class called "MyClass"; the instance handle is used to tell them apart.

    There is an exception to the above rule, however. If you pass the CS_GLOBALCLASS flag when registering the class, then the window manager will ignore the instance handle when looking for your class. All of the USER32 classes are registered as global. Consequently, all of the following calls create the USER32 edit control:

    CreateWindow("edit", ..., hinstA, ...);
    CreateWindow("edit", ..., hinstB, ...);
    CreateWindow("edit", ..., hinstC, ...);

    If you are registering a class for other modules to use in dialog boxes, you need to register as CS_GLOBALCLASS, because as we saw earlier the internal CreateWindow call performed during dialog box creation to create the controls passes the dialog's HINSTANCE as the HINSTANCE parameter. Since the dialog instance handle is typically the DLL that is creating the dialog (since that same HINSTANCE is used to look up the template), failing to register with the CS_GLOBALCLASS flag means that the window class lookup will not find the class since it's registered under the instance handle of the DLL that provided the class, not the one that is using it.

    In 16-bit Windows, the instance handle did other things, too, but they are no longer relevant to Win32.

    A common mistake is to pass the HINSTANCE of some other module (typically, the primary executable) when registering a window class. Now that you understand what the HINSTANCE is used for, you should be able to explain the consequences of registering a class with the wrong HINSTANCE.

  • The Old New Thing

    News flash: Everybody has to pay income tax


    NFL rookies are required to attend "How not to mess up your life like those other professional athletes" training. They learn about such things as sexual harassment, AIDS, common-law marriage, and, of course, taxes.

    Kendrell Bell, a Pittsburgh Steelers linebacker, tells of his great awakening to the verities of income tax: "I got a million-dollar signing bonus. But then I got the check, and it was only $624,000. I thought, Oh, well, I'll get the other half later. Then I found out that's all there was. I thought, They can't do this to me. Then I got on the Internet and I found out they can."

    Shocking! Football players have to pay income tax! Where will the injustice end?

    [Update: Yes, this is an inadvertent repeat. The link is better, though, not requiring a New York Times subscription.]

  • The Old New Thing

    Tweaking our computation of the interval between two moments in time


    We can take our computation of the interval between two moments in time and combine it with the trick we developed for using the powers of mathematics to simplify multi-level comparisons to reduce the amount of work we impose upon the time/date engine.

    static void PrintAge(DateTime bday, DateTime asof)
     // Accumulate years without going over.
     int years = asof.Year - bday.Year;
     if (asof.Month*32 + asof.Day < bday.Month*32 + bday.Day) years--;
     DateTime t = bday.AddYears(years);
     // Accumulate months without going over.
     int months = asof.Month - bday.Month;
     if (asof.Day < bday.Day) months--;
     months = (months + 12) % 12;
     t = t.AddMonths(months);
     // Days are constant-length, woo-hoo!
     int days = (asof - t).Days;
     SC.WriteLine("{0} years, {1} months, {2} days",
                  years, months, days);

    Observe that we avoided a call to the AddYears method (which is presumably rather complicated because years are variable-length) by replacing it with a multi-level comparison to determine whether the ending month/day falls later in the year than the starting month/day. Since no month has 32 days, a multiplier of 32 is enough to avoid an overflow of the day into the month field of the comparison key.

  • The Old New Thing

    Computing the interval between two moments in time


    Computing the interval between two moments in time is easy: It's just subtraction, but subtraction may not be what you want.

    If you are displaying time units on the order of months and years, then you run into the problem that a month is of variable length. some people just take the value relative to a base date of January 1 and extract the year and month counts.

    Unfortunately, this results in somewhat non-intuitive results. Let's illustrate with some examples. I'm going to write this in C# because it lets me focus on the algorithm instead of getting distracted by "oh dear how do I convert between SYSTEMTIME and FILETIME?" issues, and because it hightlights some new issues.

    // Remember, code in italics is wrong
    using System;
    using SC = System.Console;
    class Program {
     static void PrintAge(DateTime bday, DateTime asof)
      TimeSpan span = asof - bday;
     public static void Main(string[] args) {
      DateTime bday = DateTime.Parse(args[0]);
      DateTime asof = DateTime.Parse(args[1]);
      if (bday > asof) { SC.WriteLine("not born yet"); return; }
      PrintAge(bday, asof);

    The two parameters to the program are the victim's birthday and the date as of which you want to compute the victim's age.

    Here's a sample run:

    > howold 1/1/2001 1/1/2002

    Observe that the TimeSpan structure does not attempt to produce results in any unit larger than a day, since the authors of TimeSpan realized that months and years are variable-length.

    A naive implementation might go like this:

    static void PrintAge(DateTime bday, DateTime asof)
     TimeSpan span = asof - bday;
     DateTime dt = (new DateTime(1900, 1, 1)).Add(span);
     SC.WriteLine("{0} years, {1} months, {2} days",
                  dt.Year - 1900, dt.Month - 1, dt.Day - 1);

    Try it with some command lines and see what happens:

    > howold 1/1/2001 1/1/2002
    1 years, 0 months, 0 days // good
    > howold 1/1/2001 3/1/2001
    0 years, 2 months, 0 days // good
    > howold 1/1/2000 1/1/2001
    1 years, 0 months, 1 days // wrong
    > howold 9/1/2000 11/1/2000
    0 years, 2 months, 2 days // wrong

    Why does it say that a person born on January 1, 2000 is one year and one day old on January 1, 2001? The person is clearly exactly one year old on that day. Similarly, it thinks that November first is two months and two days after September first, when it is clearly two months exactly.

    The reason is that months and years are variable-length, but our algorithm assumes that they are constant. Specifically, months and years are context-sensitive but the algorithm assumes that they are translation-invariant. The lengths of months and years depend which month and year you're talking about. Leap years are longer than non-leap years. Months have all different lengths.

    How do you fix this? Well, first you have to figure out how human beings compute the difference between dates when variable-length units are involved. The most common algorithm is to declare that one year has elapsed when the same month and day have arrived in the year following the starting point. Similarly, a month has elapsed when the same numerical date has arrived in the month following the starting point.

    Mentally, you add years until you can't add years any more without overshooting. Then you add as many months as fit, and then finish off with days. (Some people subtract, but the result is the same.)

    Now you get to mimic this algorithm in code.

    static void PrintAge(DateTime bday, DateTime asof)
     // Accumulate years without going over.
     int years = asof.Year - bday.Year;
     DateTime t = bday.AddYears(years);
     if (t > asof) { years--; t = bday.AddYears(years); }
     // Accumulate months without going over.
     int months = asof.Month - bday.Month; // fixed 10pm
     if (asof.Day < bday.Day) months--;
     months = (months + 12) % 12;
     t = t.AddMonths(months);
     // Days are constant-length, woo-hoo!
     int days = (asof - t).Days;
     SC.WriteLine("{0} years, {1} months, {2} days",
                  years, months, days);

    Notice that this algorithm agrees with the common belief that people born on February 29th have birthdays only once every four years.

    Exercise: Explain what goes wrong if you change the line

     if (t > asof) { years--; t = bday.AddYears(years); }


     if (t > asof) { years--; t = t.AddYears(-1); }
  • The Old New Thing

    Using the powers of mathematics to simplify multi-level comparisons


    What a boring title.

    Often you'll find yourself needing to perform a multi-level comparison. The most common example of this is performing a version check when there are major and minor version numbers involved. Bad version number checks are one of the most common sources of errors.

    If you're comparing version numbers, you can use the VerifyVersionInfo function to do the version check for you, assuming you don't need to run on operating systems prior to Windows 2000.

    Instead of writing a multi-level comparison, you can pack the values into a single comparison. Consider:

    inline unsigned __int64
    MakeUINT64(DWORD Low, DWORD High)
      Value.LowPart = Low;
      Value.HighPart = High;
      return Value.QuadPart;
    BOOL IsVersionAtLeast(DWORD Major, DWORD Minor,
                          DWORD MajorDesired, DWORD MinorDesired)
      return MakeUINT64(Minor, Major) >= MakeUINT64(MinorDesired, MajorDesired);

    What happened here?

    We took the two 32-bit values and combined them into a large 64-bit value, putting the most significant portion in the high-order part and the less significant portion in the lower-order part.

    Then we sit back and let the power of mathematics do our work for us. If you remember the rules for comparisons from grade school, you'll realize that they exactly match the rules we want to apply to our multi-level comparison. Compare the major values; if different, then that's the result. Otherwise, compare the minor values.

    If you still don't believe it, look at the generated code:

      00000 8b 44 24 04      mov     eax, DWORD PTR _Major$[esp-4]
      00004 3b 44 24 0c      cmp     eax, DWORD PTR _MajorDesired$[esp-4]
      00008 8b 4c 24 08      mov     ecx, DWORD PTR _Minor$[esp-4]
      0000c 8b 54 24 10      mov     edx, DWORD PTR _MinorDesired$[esp-4]
      00010 72 0b            jb      SHORT $L48307
      00012 77 04            ja      SHORT $L48317
      00014 3b ca            cmp     ecx, edx
      00016 72 05            jb      SHORT $L48307
      00018 33 c0            xor     eax, eax
      0001a 40               inc     eax
      0001b eb 02            jmp     SHORT $L48308
      0001d 33 c0            xor     eax, eax
      0001f c2 10 00         ret     16                     ; 00000010H

    The code generated by the compiler is equivalent to

    BOOL IsVersionAtLeastEquiv(DWORD Major, DWORD Minor,
                          DWORD MajorDesired, DWORD MinorDesired)
     if (Major < MajorDesired) return FALSE;
     if (Major > MajorDesired) return TRUE;
     if (Minor < MinorDesired) return FALSE;
     return TRUE;

    In fact, if you had written the code the (error-prone) old-fashioned way, you would have gotten this:

    BOOL IsVersionAtLeast2(DWORD Major, DWORD Minor,
                           DWORD MajorDesired, DWORD MinorDesired)
      return Major > MajorDesired ||
       (Major == MajorDesired && Minor >= MinorDesired);
      00000 55               push    ebp
      00001 8b ec            mov     ebp, esp
      00003 8b 45 08         mov     eax, DWORD PTR _Major$[ebp]
      00006 3b 45 10         cmp     eax, DWORD PTR _MajorDesired$[ebp]
      00009 77 0e            ja      SHORT $L48329
      0000b 75 08            jne     SHORT $L48328
      0000d 8b 45 0c         mov     eax, DWORD PTR _Minor$[ebp]
      00010 3b 45 14         cmp     eax, DWORD PTR _MinorDesired$[ebp]
      00013 73 04            jae     SHORT $L48329
      00015 33 c0            xor     eax, eax
      00017 eb 03            jmp     SHORT $L48330
      00019 33 c0            xor     eax, eax
      0001b 40               inc     eax
      0001c 5d               pop     ebp
      0001d c2 10 00         ret     16                     ; 00000010H

    which is, as you can see, functionally identical to both previous versions.

    You can also pack the values into smaller units, provided you know that there will be no overflow or truncation. For example, if you know that the Major and Minor values will never exceed 65535, you could have used the following:

    BOOL SmallIsVersionAtLeast(WORD Major, WORD Minor,
                               WORD MajorDesired, WORD MinorDesired)
     return MAKELONG(Minor, Major) >= MAKELONG(MinorDesired, MajorDesired);
     00000 0f b7 44 24 0c   movzx   eax, WORD PTR _MajorDesired$[esp-4]
     00005 0f b7 4c 24 10   movzx   ecx, WORD PTR _MinorDesired$[esp-4]
     0000a 0f b7 54 24 08   movzx   edx, WORD PTR _Minor$[esp-4]
     0000f c1 e0 10         shl     eax, 16                        ; 00000010H
     00012 0b c1            or      eax, ecx
     00014 0f b7 4c 24 04   movzx   ecx, WORD PTR _Major$[esp-4]
     00019 c1 e1 10         shl     ecx, 16                        ; 00000010H
     0001c 0b ca            or      ecx, edx
     0001e 33 d2            xor     edx, edx
     00020 3b c8            cmp     ecx, eax
     00022 0f 9d c2         setge   dl
     00025 8b c2            mov     eax, edx
     00027 c2 10 00         ret     16                     ; 00000010H

    And if you know that the versions will never exceed 255, then you can go even smaller:

    BOOL TinyIsVersionAtLeast(BYTE Major, BYTE Minor,
                              BYTE MajorDesired, BYTE MinorDesired)
     return MAKEWORD(Minor, Major) >= MAKEWORD(MinorDesired, MajorDesired);
      00000 33 c0            xor     eax, eax
      00002 8a 64 24 0c      mov     ah, BYTE PTR _MajorDesired$[esp-4]
      00006 33 c9            xor     ecx, ecx
      00008 8a 6c 24 04      mov     ch, BYTE PTR _Major$[esp-4]
      0000c 8a 44 24 10      mov     al, BYTE PTR _MinorDesired$[esp-4]
      00010 8a 4c 24 08      mov     cl, BYTE PTR _Minor$[esp-4]
      00014 66 3b c8         cmp     cx, ax
      00017 1b c0            sbb     eax, eax
      00019 40               inc     eax
      0001a c2 10 00         ret     16                     ; 00000010H

    Why would you ever need to go smaller if the original version works anyway? Because you might want to make a three-way or four-way comparison, and packing the values smaller allows you to squeeze more keys into the comparison.

    BOOL IsVersionBuildAtLeast(
        WORD Major, WORD Minor, DWORD Build,
        WORD MajorDesired, WORD MinorDesired, DWORD BuildDesired)
     return MakeUINT64(Build, MAKELONG(Minor, Major)) >=
      MakeUINT64(Build, MAKELONG(MinorDesired, MajorDesired));

    By packing the major version, minor version, and build number into a single 64-bit value, a single comparison operation will compare all three at once. Compare this to the complicated (and teetering-towards unreadable) chain of comparisons you would normally have to write:

      return Major > MajorDesired ||
       (Major == MajorDesired &&
        (Minor >= MinorDesired ||
         (Minor == MinorDesired && Build >= BuildDesired)));
  • The Old New Thing

    There's an awful lot of overclocking out there


    A bunch of us were going through some Windows crashes that people sent in by clicking the "Send Error Report" button in the crash dialog. And there were huge numbers of them that made no sense whatsoever. For example, there would be code sequences like this:

       mov ecx, dword ptr [someValue]
       mov eax, dword ptr [otherValue]
       cmp ecx, eax
       jnz generateErrorReport

    Yet when we looked at the error report, the ecx and eax registers were equal! There were other crashes of a similar nature, where the CPU simply lots its marbles and did something "impossible".

    We had to mark these crashes as "possibly hardware failure". Since the crash reports are sent anonymously, we have no way of contacting the submitter to ask them follow-up questions. (The ones that the group I was in was investigating were failures that were hit only once or twice, but were of the type that were deemed worthy of close investigation because the types of errors they uncovered—if valid—were serious.)

    One of my colleagues had a large collection of failures where the program crashed at the instruction

      xor eax, eax

    How can you crash on an instruction that simply sets a register to zero? And yet there were hundreds of people crashing in precisely this way.

    He went through all the published errata to see whether any of them would affect an "xor eax, eax" instruction. Nothing.

    He sent email to some Intel people he knew to see if they could think of anything. [Aside from overclocking, of course. - Added because people apparently take my stories hyperliterally and require me to spell out the tiniest detail, even the stuff that is so obvious that it should go without saying. I didn't want to give away the story's punch line too soon!] They said that the only [other] thing they could think of was that perhaps somebody had mis-paired RAM on their motherboard, but their description of what sorts of things go wrong when you mis-pair didn't match this scenario.

    Since the failure rate for this particular error was comparatively high (certainly higher than the one or two I was getting for the failures I was looking at), he requested that the next ten people to encounter this error be given the opportunity to leave their email address and telephone number so that he could call them and ask follow-up questions. Some time later, he got word that ten people took him up on this offer, and he sent each of them e-mail asking them various questions about their hardware configurations, including whether they were overclocking. [- Continuing from above aside: See? Obviously overclocking was considered as a possibility.]

    Five people responded saying, "Oh, yes, I'm overclocking. Is that a problem?"

    The other half said, "What's overclocking?" He called them and walked them through some configuration information and was able to conclude that they were indeed all overclocked. But these people were not overclocking on purpose. The computer was already overclocked when they bought it. These "stealth overclocked" computers came from small, independent "Bob's Computer Store"-type shops, not from one of the major computer manufacturers or retailers.

    For both groups, he suggested that they stop overclocking or at least not overclock as aggressively. And in all cases, the people reported that their computer that used to crash regularly now runs smoothly.

    Moral of the story: There's a lot of overclocking out there, and it makes Windows look bad.

    I wonder if it'd be possible to detect overclocking from software and put up a warning in the crash dialog, "It appears that your computer is overclocked. This may cause random crashes. Try running the CPU at its rated speed to improve stability." But it takes only one false positive to get people saying, "Oh, there goes Microsoft blaming other people for its buggy software again."

  • The Old New Thing

    The end of one of the oldest computers at Microsoft still doing useful work


    My building was scheduled for a carpet replacement—in all my years at Microsoft, I think this is the first time this has ever happened to a building I was in—so we all had to pack up our things so the carpeters could get clear access to the floor. You go through all the pain of an office move (packing all your things) but don't get the actual reward of a new office.

    One of the machines in my office probably ranked high on the "oldest computer at Microsoft still doing useful work" charts. It was a 50MHz 486 with 12MB of memory and 500 whole megabytes of disk space. (Mind you, it wasn't born this awesome. It started out with only 8MB of memory and 200MB of disk space, but I upgraded it after a few years.) This machine started out its life as a high-end Windows 95 test machine, then when its services were no longer needed, I rescued it from the scrap heap and turned it into my little web server where among other things, Microsoft employees could read my blog article queue months before publication. It also served as my "little computer for doing little things". For example, the Internet Explorer test team used it for FTP testing since I installed a custom FTP server onto it. (Therefore, I could make it act like any type of server, or like a completely bizarro server if a security scenario required it.) It also housed various "total wastes of time" such as the "What's Raymond doing right now?" program, and the "Days without a pony" web page.

    I added a CD-ROM drive, which cost me $200. This was back in the days when getting a CD-ROM drive meant plugging in a custom ISA card and installing a MS-DOS driver into the CONFIG.SYS file. Like an MS-DOS driver gets you anywhere any more. I had to write my own driver for it.

    I took it as a challenge to see how high I could get the machine's uptime. Once the hardware stabilized (which went a lot quicker once I gave up trying to get the old network card to stop wedging and just bought a new one), I put it on a UPS that had been gifted to me in exchange for debugging why the company's monitoring software wasn't working on Windows 95. Whenever I had to move offices, I found somebody who wasn't moving and relocated the computer there for a few days. The UPS kept the machine running while I carted it down the hall or into the next building. I think I got the uptime as high as three years before the building suffered a half-day power outage that drained the UPS.

    A few years later, the machine started rebooting for no apparent reason. Turns out the UPS battery itself was dying and generating its own mini-power outages. Ironic that a UPS ended up creating power outages instead of masking them. But on the other hand, it was free, so I can't complain. Without a UPS, the machine became victim of building-wide power outages and office moves.

    Over the years, more and more parts of the machine started to wear out and had to be worked around. The CMOS battery eventually died, so restarting the computer after an outage involved lots of typing. (It always thought the date was January 1983.) The clock also drifted, so I wrote a program to re-synchronize it automatically every few days.

    When I packed up the computer for the recarpeting, I assumed that afterwards, it would fire back up like the trooper it was. But alas, it just sat there. After much fiddling and removal of non-critical hardware, I got it to power on. Now it complains "no boot device".

    The hard drive (or perhaps the hard drive controller) had finally died. The shock of being shut off and restarted proved to be its downfall. Since it's nearly impossible to find replacement parts for a computer this old, I'm going to have to return it to the scrap heap.

    Good-bye, old friend. But you won't be forgotten. I'm going to transfer your name and IP address to another computer I rescued from the scrap heap many years ago for just this eventuality. But still no mouse.

    (Alas, this was the first of a series of computers to reach retirement age within days of each other. Perhaps I'll eulogize those other machines someday.)

Page 2 of 3 (26 items) 123