May, 2007

  • The Old New Thing

    Suggestion Box 3


    Post suggestions for future topics here instead of posting off-topic comments. Note that the suggestion box is emptied and read periodically so don't be surprised if your suggestion vanishes. (Note also that I am under no obligation to accept any suggestion.)

    Topics I are more inclined to cover:

    • Windows history (particularly the Windows 95 era).
    • Windows user interface programming in Win32, and shell programming in particular.
    • General programming topics (selectively).
    • Issues of general interest.
    • My personal hobbies.

    Topics I am not inclined to cover:

    • The blog software itself. You can visit the Community Server home page and cruise their support forums.
    • Internet Explorer. You can try the IE folks.
    • Visual Studio. You can try one of the Visual Studio blogs.
    • Managed code. This is not a .NET blog. I do not work on .NET technologies. As far as .NET is concerned, I'm just another programmer like you. Occasionally I touch a .NET-related topic, but I do not bring any expertise to the subject.
    • Non-software Microsoft topics, such as product support policies, marketing tactics, jobs and careers, legal issues.
    • Microsoft software that isn't Windows. (Exchange, Office, ...)
    • Windows topics outside user interface programming. (Plug and Play, Terminal Services, Windows Messenger, Outlook Express, SQL, IIS, remoting, SOA...)
    • User interface programming in anything other than Win32. (Because I know nothing about it.)
    • Debugging a specific problem. (Not of general interest.)
    • Predictions for the future. (What's the title of this blog again?)
    • Participation in Internet memes.

    You can also send feedback on Microsoft products directly to Microsoft. All the feedback gets read, even the death threats.

    Suggestions should be between two and four sentences in length. As you can see, there are hundreds of them already, so you have three seconds to get your point across. Please also search the blog first because your suggestion may have already been covered. And remember, questions aren't suggestions.

    Note the enormous topic backlog. Consequently, the suggestion box has been closed temporarily and will reopen once the existing backlog has cleared, which I estimate will happen sometime in early 2010. If your suggestion is that important, I'm sure you'll remember it when the suggestion box reopens.

  • The Old New Thing

    Quick overview of how processes exit on Windows XP


    Exiting is one of the scariest moments in the lifetime of a process. (Sort of how landing is one of the scariest moments of air travel.)

    Many of the details of how processes exit are left unspecified in Win32, so different Win32 implementations can follow different mechanisms. For example, Win32s, Windows 95, and Windows NT all shut down processes differently. (I wouldn't be surprised if Windows CE uses yet another different mechanism.) Therefore, bear in mind that what I write in this mini-series is implementation detail and can change at any time without warning. I'm writing about it because these details can highlight bugs lurking in your code. In particular, I'm going to discuss the way processes exit on Windows XP.

    I should say up front that I do not agree with many steps in the way processes exit on Windows XP. The purpose of this mini-series is not to justify the way processes exit but merely to fill you in on some of the behind-the-scenes activities so you are better-armed when you have to investigate into a mysterious crash or hang during exit. (Note that I just refer to it as the way processes exit on Windows XP rather than saying that it is how process exit is designed. As one of my colleagues put it, "Using the word design to describe this is like using the term swimming pool to refer to a puddle in your garden.")

    When your program calls ExitProcess a whole lot of machinery springs into action. First, all the threads in the process (except the one calling ExitProcess) are forcibly terminated. This dates back to the old-fashioned theory on how processes should exit: Under the old-fashioned theory, when your process decides that it's time to exit, it should already have cleaned up all its threads. The termination of threads, therefore, is just a safety net to catch the stuff you may have missed. It doesn't even wait two seconds first.

    Now, we're not talking happy termination like ExitThread; that's not possible since the thread could be in the middle of doing something. Injecting a call to ExitThread would result in DLL_THREAD_DETACH notifications being sent at times the thread was not prepared for. Nope, these threads are terminated in the style of TerminateThread: Just yank the rug out from under it. Buh-bye. This is an ex-thread.

    Well, that was a pretty drastic move, now, wasn't it. And all this after the scary warnings in MSDN that TerminateThread is a bad function that should be avoided!

    Wait, it gets worse.

    Some of those threads that got forcibly terminated may have owned critical sections, mutexes, home-grown synchronization primitives (such as spin-locks), all those things that the one remaining thread might need access to during its DLL_PROCESS_DETACH handling. Well, mutexes are sort of covered; if you try to enter that mutex, you'll get the mysterious WAIT_ABANDONED return code which tells you that "Uh-oh, things are kind of messed up."

    What about critical sections? There is no "Uh-oh" return value for critical sections; EnterCriticalSection doesn't have a return value. Instead, the kernel just says "Open season on critical sections!" I get the mental image of all the gates in a parking garage just opening up and letting anybody in and out. [See correction.]

    As for the home-grown stuff, well, you're on your own.

    This means that if your code happened to have owned a critical section at the time somebody called ExitProcess, the data structure the critical section is protecting has a good chance of being in an inconsistent state. (Afer all, if it were consistent, you probably would have exited the critical section! Well, assuming you entered the critical section because you were updating the structure as opposed to reading it.) Your DLL_PROCESS_DETACH code runs, enters the critical section, and it succeeds because "all the gates are up". Now your DLL_PROCESS_DETACH code starts behaving erratically because the values in that data structure are inconsistent.

    Oh dear, now you have a pretty ugly mess on your hands.

    And if your thread was terminated while it owned a spin-lock or some other home-grown synchronization object, your DLL_PROCESS_DETACH will most likely simply hang indefinitely waiting patiently for that terminated thread to release the spin-lock (which it never will do).

    But wait, it gets worse. That critical section might have been the one that protects the process heap! If one of the threads that got terminated happened to be in the middle of a heap function like HeapAllocate or LocalFree, then the process heap may very well be inconsistent. If your DLL_PROCESS_DETACH tries to allocate or free memory, it may crash due to a corrupted heap.

    Moral of the story: If you're getting a DLL_PROCESS_DETACH due to process termination,† don't try anything clever. Just return without doing anything and let the normal process clean-up happen. The kernel will close all your open handles to kernel objects. Any memory you allocated will be freed automatically when the process's address space is torn down. Just let the process die a quiet death.

    Note that if you were a good boy and cleaned up all the threads in the process before calling ExitThread, then you've escaped all this craziness, since there is nothing to clean up.

    Note also that if you're getting a DLL_PROCESS_DETACH due to dynamic unloading, then you do need to clean up your kernel objects and allocated memory because the process is going to continue running. But on the other hand, in the case of dynamic unloading, no other threads should be executing code in your DLL anyway (since you're about to be unloaded), so—assuming you coded up your DLL correctly—none of your critical sections should be held and your data structures should be consistent.

    Hang on, this disaster isn't over yet. Even though the kernel went around terminating all but one thread in the process, that doesn't mean that the creation of new threads is blocked. If somebody calls CreateThread in their DLL_PROCESS_DETACH (as crazy as it sounds), the thread will indeed be created and start running! But remember, "all the gates are up", so your critical sections are just window dressing to make you feel good.

    (The ability to create threads after process termination has begun is not a mistake; it's intentional and necessary. Thread injection is how the debugger breaks into a process. If thread injection were not permitted, you wouldn't be able to debug process termination!)

    Next time, we'll see how the way process termination takes place on Windows XP caused not one but two problems.


    †Everybody reading this article should already know how to determine whether this is the case. I'm assuming you're smart. Don't disappoint me.

  • The Old New Thing

    The old-fashioned theory on how processes exit


    Life was simpler back in the old days.

    Back in the old days, processes were believed to be in control of their threads. You can see this in the "old fashioned" way of exiting a process, namely by exiting all the threads. This method works only if the process knows about all the threads running in it and can get each one to clean up when it's time for the process to exit.

    In other words, the old-fashioned theory was that when a process wanted to exit, it would do something like this:

    • Tell all its threads that the show is over.
    • Wait for each of those threads to finish up.
    • Exit the main thread (which therefore exits the process).

    Of course, that was before the introduction of programming constructions that created threads that the main program didn't know about and therefore had no control over. Things like the thread pool, RPC worker threads, and DLLs that create worker threads (something still not well-understood even today).

    The world today is very different. Next time, we'll look at how this simple view of processes and threads affects the design of how processes exit.

    Still, you learned enough today to be able to solve this person's problem.

  • The Old New Thing

    How my lack of understanding of how processes exit on Windows XP forced a security patch to be recalled


    Last year, a Windows security update got a lot of flack for causing some machines to hang, and it was my fault. (This makes messing up a demo at the Financial Analysts Meeting look like small potatoes.)

    The security fix addressed a category of attacks wherein people could construct shortcut files or other items which specified a CLSID that was never intended to be used as a shell extension. As we saw earlier, lots of people mess up IUnknown::QueryInterface, and if you pass the CLSID of one of these buggy implementations, Explorer would dutifully create it and try to use it, and then bad things would happen. The object might crash or hang or even corrupt memory and keep running (sort of).

    To protect against buggy shell extensions, Explorer was modified to use a helper program called verclsid.exe whose job was to be the "guinea pig" and host the shell extension and do some preliminary sniffing around to make sure the shell extension passed some basic functionality tests before letting it run loose in Explorer. That way, if the shell extension went crazy, the victim would be the verclsid.exe process and not the main Explorer process.

    The verclsid.exe program created a watchdog thread: If the preliminary sniffing took too long, the watchdog assumed that the shell extension was hung and the watchdog told Explorer, "Don't use this shell extension."

    I was one of the people brought in to study this new behavior, poke holes in its design, poke holes in its implementation, review every line of code that changed and make sure that it did exactly what it was supposed to do without introducing any new bugs along the way. We found some issues, testers found some other issues, and all the while, the clock was ticking since this was a security patch and people enjoy mocking Microsoft over how long it takes to put a security patch together.

    The patch went out, and reports started coming in that machines were hanging. How could that be? We created a watchdog thread specifically to catch the buggy shell extensions that hung; why isn't the watchdog thread doing its job?

    That was a long set-up for today's lesson.

    After running its sanity tests, the verclsid.exe program releases the shell extension, un-initializes COM, and then calls ExitProcess with a special exit code that means, "All tests passed." If you read yesterday's installment, you already know where I messed up.

    The DLL that implemented the shell extension created a worker thread, so it did an extra LoadLibrary on itself so that it wouldn't get unloaded when COM freed it as part of CoUninitialize tear-down. When the DLL got its DLL_PROCESS_DETACH, it shut down its worker thread by the common technique of setting a "clean up now" event that the worker thread listened for, and then waiting for the worker thread to respond with a "Okay, I'm all done" event.

    But recall that the first stage in process exit is the termination of all threads other than the one that called ExitProcess. That means that the DLL's worker thread no longer exists. After setting the event to tell the (nonexistent) thread to clean up, it then waited for the (nonexistent) thread to say that it was done. And since there was nobody around listening for the clean-up event, the "all done" event never got set. The DLL hung in its DLL_PROCESS_DETACH.

    Why didn't our watchdog thread save us? Because the watchdog thread got killed too!

    Now, the root cause for all this was a buggy shell extension that did bad things in its DLL_PROCESS_DETACH, but blaming the shell extension misses the point. After all, it was the fact that there existed buggy shell extensions that created the need for the verclsid.exe program in the first place.

    Welcome Slashdot readers. Since you won't read the existing comments before posting your own, I'll float some of the more significant ones here.

    The buggy shell extension was included with a printer driver for a printer that is no longer manufactured. Good luck finding one of those in your test suite.

    The security update was recalled and reissued in a single action, which most people would call an update or refresh, but the word recall works better in a title.

  • The Old New Thing

    Performing an operation in each subdirectory of a directory tree from batch


    To execute a command in each subdirectory of a directory tree from a batch file, you can adapt the following:

    for /f "delims=" %%i in ('dir /ad/s/b') do echo %%i

    (If you want to play with this command from the command prompt, then undouble the percent signs.)

    The /F option enables various special behaviors of the FOR command. The most important change is that a string in single-quotation marks causes the contents to be interpreted as a command whose output is to be parsed. (This behavior changes if you use the usebackq option, but I'm not using that here.) Therefore, the FOR command will run the dir /ad/s/b command and parse the output. The dir /ad/s/b command performs a recursive listing of only directories, printing just the names of the directories found.

    The option we provide, delims= changes the default delimiter from a space to nothing. This means that the entire line is to be read into the %i variable. (Normally, only the first word is assigned to %i.) Therefore, the FOR loop executes once for each subdirectory, with the %i variable set to the subdirectory name.

    The command request to be performed for each line is simply echoing the directory name. In real life, you would probably put something more interesting here. For example, to dump the security descriptor of each directory (which was the original problem that inspired this entry), you can type this on the command line:

    for /f "delims=" %i in ('dir /ad/s/b') do cacls "%i" >>"%TEMP%\cacls.log"

    Nitpicker's corner

    I doubt anybody actually enjoys working with batch files, but that doesn't mean tips on using it more effectively aren't valid.

  • The Old New Thing

    Why are console windows limited to Lucida Console and raster fonts?


    In Windows 95, we experimented with other fonts for the console window, and it was a disaster.

    In order to be a usable font for the console window, the font needs to be more than merely monospace. It also needs to support all the characters in the OEM code page. Testing this is easy for SBCS code pages, since they have only 256 characters. But for DBCS code pages, testing all the characters means testing tens of thousands of code points. The OEM code page test already rules out a lot of fonts, because the 437 code page (default in the United States) contains oddball characters like the box-drawing characters and a few astronomical symbols which most Windows fonts don't bother to include.

    But checking whether the font supports all the necessary characters is a red herring. The most common reason why a font ends up unsuitable for use in a console window is that the font contains characters with negative A- or C-widths. These A- and C-width values come from the ABC structure and represent the amount of under- and overhang a character consumes.

    Consider, for example, the capital letter W. In many fonts, this character contains both under- and overhang:

    X      X       X
    X     X  X     X
     X    X  X    X 
     X    X  X    X 
     X    X  X    X 
      X  X    X X   
      X  X    X X   
      X  X    X X   
        X      X    
        X      X    

    Notice how the left and right stems "stick out" beyond the putative cell boundaries.

    I wrote code in Windows 95 to allow any monospace font to be used in console windows, and the ink was hardly even dry on the CD before the bugs started pouring in. "When I choose Courier New as my font, my console window looks like a Jackson Pollock painting with splotches of pixels everywhere, and parts of other characters get cut off." (Except that they didn't use words as nice as "splotches of pixels".)

    The reason is those overhang pixels. The console rendering model assumes each character fits neatly inside its fixed-sized cell. When a new character is written to a cell, the old cell is overprinted with the new character, but if the old character has overhang or underhang, those extra pixels are left behind since they "spilled over" the required cell and infected neighbor cells. Similarly, if a neighboring character "spilled over", those "spillover pixels" would get erased.

    The set of fonts that could be used in the console window was trimmed to the fonts that were tested and known to work acceptably in console windows. For English systems, this brought us down to Lucida Console and Terminal.

    "Why isn't there an interface for choosing a replacement font, with a big annoying message box warning you that 'Choosing a font not on the list above may result in really ugly results. Don't blame me!'?"

    First of all, because we know that nobody reads those warnings anyway. Second, because a poor choice of font results in the console window looking so ugly that everybody would rightly claim that it was a bug.

    "No, it's not a bug. You brought this upon yourself by choosing a font that results in painting artifacts when used in a console window."

    "Well, that's stupid. You should've stopped me from choosing a font that so clearly results in nonsense."

    And that's what we did.

    Of course, if you're a super-geek and are willing to shoulder the blame if the font you pick happens not to be suitable for use in a console window, you can follow the instructions in this Knowledge Base article to add your font to the list. But if you end up creating a work of modern art, well, you asked for it.

    Nitpicker's corner

    In the title of this entry, s/console windows/Windows console windows/†

    †s/Windows console windows/Windows console windows when displayed inside a GUI window, as opposed to consoles that have gone to hardware fullscreen, which is another matter entirely/.

  • The Old New Thing

    Visual C++ 2005 will generate manifests for you


    New in Visual C++ 2005 is the ability to specify a manifest dependency via a #pragma directive. This greatly simplifies using version 6 of the shell common controls. You just have to drop the line

    // do not use - see discussion below
    #pragma comment(linker, \
        "\"/manifestdependency:type='Win32' "\
        "name='Microsoft.Windows.Common-Controls' "\
        "version='' "\
        "processorArchitecture='X86' "\
        "publicKeyToken='6595b64144ccf1df' "\

    into your program and the linker will do the rest.

    Note that the processor architecture is hard-coded into the above directive, which means that if you are targetting x64, you'll get the wrong manifest. To fix that, we need to do some preprocessor munging.

    #if defined(_M_IX86)
    #elif defined(_M_AMD64)
    #elif defined(_M_IA64)
    #error Unknown processor architecture.
    #pragma comment(linker, \
        "\"/manifestdependency:type='Win32' "\
        "name='Microsoft.Windows.Common-Controls' "\
        "version='' "\
        "processorArchitecture='" MANIFEST_PROCESSORARCHITECTURE "' "\
        "publicKeyToken='6595b64144ccf1df' "\

    Update: I didn't know that * is allowed here to indicate "all architectures". That simplifies matters greatly.

    #pragma comment(linker, \
        "\"/manifestdependency:type='Win32' "\
        "name='Microsoft.Windows.Common-Controls' "\
        "version='' "\
        "processorArchitecture='*' "\
        "publicKeyToken='6595b64144ccf1df' "\

    Nitpicker's corner

    * That wasn't a footnote marker.

  • The Old New Thing

    The administrator is an idiot


    Nearly all computer administrators are idiots.

    That's not because the personnel department is incompetent or because it's impossible to train competent administrators. It's because, for a consumer operating system, the computer administrator didn't ask to be one. In nearly all cases, the computer administrator is dad or grandma.† They didn't ask to be to be the computer administrator. They just want to surf the web and read email from Jimmy.‡

    All this means is that you can't say, "Well, if the user is an administrator, as opposed to a normal user, then it's okay to show them all these dangerous things (such as critical operating system files) because they know what they're doing." Grandma doesn't know what she's doing.

    For a consumer operating system, a friendly user interface means protecting the administrators from themselves.

    Nitpicker's corner

    Sigh. One article without a nitpicker's corner and look what happens.

    †The words "dad" and "grandma" refer to archetypes for non-technical home users and are not intended to be interpreted as literally dad and grandma.

    ‡Not all grandchildren are named Jimmy.

  • The Old New Thing

    Don't be helpless: I don't know anything about MFC modal loops, but unlike some people, I'm not afraid to find out


    Commenter Tom Grelinger asks via the Suggestion Box:

    If I have a modal CDialog that is visible and usable to the user. Let's say I receive an event somewhere else in the program and I call DestroyWindow on the modal CDialog from within the event. I notice that the OnDestroy is called on the CDialog, but DoModal never exits until a WM_QUIT is posted to the modal's message pump. What are the pitfalls to this? Unfortunately, there is really no way to avoid this situation.

    I'm not sure what the question is, actually. The question as stated is "What are the pitfalls to this?" but he answered that in his own question: The pitfall is that "DoModal never exits until a WM_QUIT is posted to the modal dialog's message pump."

    I'm going to assume that the question really is, "Why doesn't destroying the window work?" with the follow-up question, "What is the correct way to dismiss a modal dialog?"

    The first problem with this question is that it assumes that I know what a CDialog is. From its name, I'm going to assume that this is an MFC class for managing a dialog box. But you don't even have to know that to answer the first reformulated question operating only from Win32 principles: DestroyWindow is not how you exit a modal dialog. You exit a modal dialog with EndDialog. The DestroyWindow technique is for modeless dialogs.

    But let's look at the question another way, which is my point for today: You have the MFC source code. Don't be afraid to read it. Especially since I don't use MFC personally; I don't even know the basic principles of application design with MFC. I work in straight Win32. As a result, I don't know the answer off the top of my head, but fifteen minutes reading the MFC source code quickly reveals the reason why destroying the window doesn't work.

    Watch me as I go and find out the answer. It's nothing you can't already do yourself.

    The CDialog::DoModal method calls CWnd::RunModalLoop to run the dialog loop. If you look at CWnd::RunModalLoop, you can see the conditions under which it will exit the modal loop. Here's the code with irrelevant details deleted. (They're irrelevant because they have nothing to do with how the modal loop exits.)

    int CWnd::RunModalLoop(DWORD dwFlags)
        ... preparatory work ...
        // acquire and dispatch messages until the modal state is done
        for (;;)
            ... code that doesn't break out of the loop ...
            // phase2: pump messages while available
                // pump message, but quit on WM_QUIT
                if (!AfxGetThread()->PumpMessage())
                    return -1;
                ... other code that doesn't break out of the loop ...
                if (!ContinueModal())
                    goto ExitModal;
                ... other code that doesn't break the loop ...
            }  while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
        return m_nModalResult;

    There are only two ways out of this loop. The first is the receipt of a WM_QUIT message. The second is if CWnd::ContinueModal decides that the modal loop is finished. The commenter already mentioned the quit message aspect to the modal loop, so that just leaves CWnd::ContinueModal.

    The CWnd::ContinueModal method is very simple:

    BOOL CWnd::ContinueModal()
        return m_nFlags & WF_CONTINUEMODAL;

    Therefore, the only other way the loop can exit is if somebody clears the WF_CONTINUEMODAL flag. A little grepping shows that there are only three places where this flag is cleared. One is in CPropertyPage, which is a derived class of CDialog and therefore isn't relevant here. (I'll ignore CPropertyPage in future searches.) The second is in the line above right after the label ExitModal. And the third is this method:

    void CWnd::EndModalLoop(int nResult)
        // this result will be returned from CWnd::RunModalLoop
        m_nModalResult = nResult;
        // make sure a message goes through to exit the modal loop
        if (m_nFlags & WF_CONTINUEMODAL)
            m_nFlags &= ~WF_CONTINUEMODAL;

    This method is called in only one place:

    void CDialog::EndDialog(int nResult)
        if (m_nFlags & (WF_MODALLOOP|WF_CONTINUEMODAL))
        ::EndDialog(m_hWnd, nResult);

    Following the money one last step, the CDialog::EndDialog method is called from four places in CDialog. It's called from CDialog::HandleInitDialog and CDialog::InitDialog if some catastrophic error occurs during dialog initialization. And it's called from CDialog::OnOK and CDialog::OnCancel in response to the user clicking the OK or Cancel buttons.

    Notice that the CDialog::EndDialog method is not called when somebody forcibly destroys the dialog from the outside.

    That's why destroying the dialog window doesn't break the modal loop. If you want to break out of the modal loop, your only choices are to post a quit message or call CWnd::EndModalLoop, either directly or indirectly (via CDialog::EndDialog, for example).

    Notice that the MFC modal loop obeys the convention on quit messages by re-posting the quit message when it breaks out of the modal loop. (Though it really should have posted the wParam from the quit message rather than just posting zero.)

    The workaround therefore is not to destroy the dialog with DestroyWindow (something you should have known not to do a priori since that's not how you exit modal dialog boxes) but rather by calling CDialog::EndDialog, passing a result code that lets the caller of CDialog::DoModal know that the dialog box exited under unusual circumstances.

    This took me fifteen minutes to research and a little over an hour to write up. All this work to answer a question that you should have been able to answer yourself with a little elbow grease. You're a smart person. Have confidence in yourself. You can do it. I know you can.

  • The Old New Thing

    The sad predicament of the unempowered manager


    I just made up that term now because I needed a word to describe the situation where some manager is put in charge of a feature but is not given a staff to implement that feature. This happens more often than you might think, since there are many features that are "horizontal", i.e., features which affect all teams throughout the project. So-called taxes often fall into this category, such as power management, accessibility, and multiple monitors. (Larry Osterman calls them *bilities. I call them taxes.)

    The unempowered manager is in a predicament, having been assigned a task without a staff to accomplish it. All the unempowered manager can do is nag other people, usually about bugs that fall into the manager's area.

    Now, most of these unempowered managers understand that they are just one of many demands on the development teams, providing advice as necessary (since they have valuable specialized knowledge about the problem area) but basically trying to stay out of the way.

    Others, on the other hand, take upon themselves a much more active role in "driving" their pet issues. This means that I will get mail like this:

    You have an elephant† bug

    The following elephant bug is assigned to you:

    16384 Elephants not available in animal dropdown box (opened 2006/05/12)

    What is the ETA for fixing this bug?

    Somebody you've never heard of

    This is another case of "You're not my manager". My manager decides what tasks I should be working on and in what order. If you think this bug should be higher on my priority list, feel free to set up a little meeting with my manager to work this out. Until then, don't bug me. I have work to do.

    "But elephant-compatibility is important."

    Are you saying that all my other tasks are unimportant? What makes elephant-compatibility more important than my other tasks? Do you even know what my other tasks are?

    At one point, this got so bad, with many managers nagging me about their favorite bugs on a nearly daily basis, that I created a SharePoint site‡ called "Raymond's task list for <date>". Whenever somebody sent me nag mail, I replied, "I have added your request to my SharePoint site and assigned it a default priority." And then I never heard from them again.

    For those new to this web site (and a reminder to those with poor memory):

    †I disguise the name because (1) it's not important to the story, and because (2) the goal is not to ridicule but rather to illustrate a point. Attempts to guess what "elephant" is will be deleted. Don't make me delete further stories in this series like I did with "Stories about Bob."

    Nitpicker's corner

    ‡Or, if you're a trademark lawyer, "A Web site powered by Microsoft® SharePoint® services."

    *Here's your stupid asterisk.

Page 1 of 5 (44 items) 12345