February, 2012

  • The Old New Thing

    Why did the Windows 95 Start button have a secret shortcut for closing it?

    • 23 Comments

    Windows 95 had a strange feature where, if you put focus on the Start button and then hit Alt+- (That's Alt and the hyphen key), you got a system menu for the Start button which let you close it, and then the Start button vanished. Programmerman wondered why this existed.

    This was not a feature; it was just a bug. The person who first wrote up the code for the Start button accidentally turned on the WS_SYS­MENU style. If you turn this style on for a child window, Windows assigns your child window a system menu. System menus for child windows may sound strange, but they are actually quite normal if you are an MDI application. And the standard hotkey for calling up the system menu of a child window is Alt+-.

    The Start button was not an MDI application, but since the WS_SYS­MENU style was set, Windows treated it like one, and when you pressed the hotkey, you got the system menu which let you close the window. (You could also move it, which was also kind of weird.)

    Let's add a button with an accidental system menu to our scratch program:

    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
        g_hwndChild = CreateWindow(
            TEXT("Button"),
            TEXT("Start"),
            WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_SYSMENU |
            BS_PUSHBUTTON | BS_CENTER | BS_VCENTER,
            0, 0, 0, 0, hwnd, (HMENU)1, g_hinst, 0);
        return TRUE;
    }
    

    Run this program, put focus on the button, and hit Alt+-. Hey look, a child window system menu.

    To fix this bug, remove the WS_SYS­MENU style. That's how the Explorer folks fixed it.

  • The Old New Thing

    How do I find out which process has a file open?

    • 24 Comments

    Classically, there was no way to find out which process has a file open. A file object has a reference count, and when the reference count drops to zero, the file is closed. But there's nobody keeping track of which processes own how many references. (And that's ignoring the case that the reference is not coming from a process in the first place; maybe it's coming from a kernel driver, or maybe it came from a process that no longer exists but whose reference is being kept alive by a kernel driver that captured the object reference.)

    This falls into the category of not keeping track of information you don't need. The file system doesn't care who has the reference to the file object. Its job is to close the file when the last reference goes away.

    You do the same thing with your COM object reference counts. All you care about is whether your reference count has reached zero (at which point it's time to destroy the object). If you later discover an object leak in your process, you don't have a magic query "Show me all the people who called AddRef on my object" because you never kept track of all the people who called AddRef on your object. Or even, "Here's an object I want to destroy. Show me all the people who called AddRef on it so I can destroy them and get them to call Release."

    At least that was the story under the classical model.

    Enter the Restart Manager.

    The official goal of the Restart Manager is to help make it possible to shut down and restart applications which are using a file you want to update. In order to do that, it needs to keep track of which processes are holding references to which files. And it's that database that is of use here. (Why is the kernel keeping track of which processes have a file open? Because it's the converse of the principle of not keeping track of information you don't need: Now it needs the information!)

    Here's a simple program which takes a file name on the command line and shows which processes have the file open.

    #include <windows.h>
    #include <RestartManager.h>
    #include <stdio.h>
    
    int __cdecl wmain(int argc, WCHAR **argv)
    {
     DWORD dwSession;
     WCHAR szSessionKey[CCH_RM_SESSION_KEY+1] = { 0 };
     DWORD dwError = RmStartSession(&dwSession, 0, szSessionKey);
     wprintf(L"RmStartSession returned %d\n", dwError);
     if (dwError == ERROR_SUCCESS) {
       PCWSTR pszFile = argv[1];
       dwError = RmRegisterResources(dwSession, 1, &pszFile,
                                     0, NULL, 0, NULL);
       wprintf(L"RmRegisterResources(%ls) returned %d\n",
               pszFile, dwError);
      if (dwError == ERROR_SUCCESS) {
       DWORD dwReason;
       UINT i;
       UINT nProcInfoNeeded;
       UINT nProcInfo = 10;
       RM_PROCESS_INFO rgpi[10];
       dwError = RmGetList(dwSession, &nProcInfoNeeded,
                           &nProcInfo, rgpi, &dwReason);
       wprintf(L"RmGetList returned %d\n", dwError);
       if (dwError == ERROR_SUCCESS) {
        wprintf(L"RmGetList returned %d infos (%d needed)\n",
                nProcInfo, nProcInfoNeeded);
        for (i = 0; i < nProcInfo; i++) {
         wprintf(L"%d.ApplicationType = %d\n", i,
                                  rgpi[i].ApplicationType);
         wprintf(L"%d.strAppName = %ls\n", i,
                                  rgpi[i].strAppName);
         wprintf(L"%d.Process.dwProcessId = %d\n", i,
                                  rgpi[i].Process.dwProcessId);
         HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
                                       FALSE, rgpi[i].Process.dwProcessId);
         if (hProcess) {
          FILETIME ftCreate, ftExit, ftKernel, ftUser;
          if (GetProcessTimes(hProcess, &ftCreate, &ftExit,
                              &ftKernel, &ftUser) &&
              CompareFileTime(&rgpi[i].Process.ProcessStartTime,
                              &ftCreate) == 0) {
           WCHAR sz[MAX_PATH];
           DWORD cch = MAX_PATH;
           if (QueryFullProcessImageNameW(hProcess, 0, sz, &cch) &&
               cch <= MAX_PATH) {
            wprintf(L"  = %ls\n", sz);
           }
          }
          CloseHandle(hProcess);
         }
        }
       }
      }
      RmEndSession(dwSession);
     }
     return 0;
    }
    

    The first thing we do is call, no wait, even before we call the Rm­Start­Session function, we have the line

     WCHAR szSessionKey[CCH_RM_SESSION_KEY+1] = { 0 };
    

    That one line of code addresses two bugs!

    First is a documentation bug. The documentation for the Rm­Start­Session function doesn't specify how large a buffer you need to pass for the session key. The answer is CCH_RM_SESSION_KEY+1.

    Second is a code bug. The Rm­­StartSession function doesn't properly null-terminate the session key, even though the function is documented as returning a null-terminated string. To work around this bug, we pre-fill the buffer with null characters so that whatever ends gets written will have a null terminator (namely, one of the null characters we placed ahead of time).

    Okay, so that's out of the way. The basic algorithm is simple:

    1. Create a Restart Manager session.
    2. Add a file resource to the session.
    3. Ask for a list of all processes affected by that resource.
    4. Print some information about each process.
    5. Close the session.

    We already mentioned that you create the session by calling Rm­Start­Session. Next, we add a single file resource to the session by calling Rm­Register­Resources.

    Now the fun begins. Getting the list of affected processes is normally a two-step affair. First, you ask for the number of affected processes (by passing 0 as the nProcInfo), then allocate some memory and call a second time to get the data. But this is just a sample program, so I've hard-coded a limit of ten processes. If more than ten processes are affected, I just give up. (You can see this if you ask for all the processes that have open handles to kernel32.dll.)

    The other tricky part is mapping the RM_PROCESS_INFO to an actual process. Since process IDs can be recycled, the RM_PROCESS_INFO structure identifies a process by the combination of the process ID and the process creation time. That combination is unique because two processes cannot have the same ID at the same time. We open the handle to the process via its ID, then confirm that the start times match. (If not, then the ID refers to a process that exited during the time we obtained the list and the time we actually looked at it.) Assuming it all matches, we get the image name and print it.

    And that's all there is to enumerating all the processes that have a particular file open. Of course, a more expressive interface for managing files in use is IFileIsInUse, which I mentioned some time ago. That interface not only tells you the application that has the file open (in a friendlier format than just an executable path), you can also use it to switch to the application and even ask it to close the file. (Windows 7 first tries IFileIsInUse, and if that fails, then it goes to the Restart Manager.)

  • The Old New Thing

    Things I've written that have amused other people, Episode 9

    • 29 Comments

    A customer liaison reported that their customer wants to be able to access their machine without needing a password. They just want to be able to net use * \\machine\share and be able to access the files right away. I guess because passwords are confusing, easy to forget, and just get in the way. Anyway, the customer discovered that they could do so on Windows XP by going to the folder they want to share, going to the Sharing tab, then clicking on the If you understand the security risks but want to share files without running the wizard link,

    and then on the Enable File Sharing dialog, clicking Just enable file sharing.

    What the customer wanted to know was if there was a way they could automate this process.

    My response to the customer liaison went like this:

    Your customer has chosen to ignore not one but two security warnings. Furthermore, since they are looking for an automated way of doing this, it sounds like they intend on deploying this "feature" to all the computers in their organization. Maybe they just enjoy being part of a botnet? Your customer is basically saying "I wish my computer to have no network security." They should at least restrict access to authenticated users. But if they if they insist on having their corporate network turned into a spam farm, they can enable the Guest account and say that it can "Access this computer from the network." Congratulations, your computers will soon be filled with malware and porn.

    That last sentence made it into some people's quotes file.

  • The Old New Thing

    When you are looking for more information, it helps to say what you need the information for

    • 46 Comments

    It's often the case that when a question from a customer gets filtered through a customer liaison, some context gets lost. (I'm giving the customer the benefit of the doubt here and assuming that it's the customer liaison that removed the context rather than the customer who never provided it.) Consider the following request:

    We would like to know more information about the method the shell uses to resolve shortcuts.

    This is kind of a vague question. It's like asking "I'd like to know more about the anti-lock braking system in my car." There are any number of pieces of information that could be provided about the anti-lock braking system.

    • "It requires a Class C data bus."
    • "The tire position sensors are on the wheel-axis."
    • "It is connected to the brakes."
    • "It is shiny."

    When we ask the customer, "Could you be more specific what type of information you are looking for?" the response is sometimes

    We want to know everything.

    This is not a helpful clarification. Do they want to start with Maxwell's Equations and build up from there?

    As it happened, in the case of wanting more information about the method the shell uses to resolve shortcuts, they just wanted to know how to disable the search-based algorithm.

    This sort of "ask for everything and figure it out later" phenomenon is quite common. I remember another customer who wanted to know "everything" about changing network passwords, and they wouldn't be any more specific than that, so we said, "Well, you can start with these documents, perhaps paying particular attention to this one, but if they tell us what they are going to be doing with the information, we can help steer them to the specific parts that will be most useful to them."

    As it turned out, all the customer really wanted to know was "When users change their password, is the new password encrypted on the wire?"

    Third example, and then I'll stop. Another customer wanted to know everything about how Explorer takes information from the file system and displays it in an Explorer window. After asking a series of questions, we eventually figured out that they in fact didn't want or need a walkthrough of the entire code path that puts results in the Explorer window. The customer simply wanted to know why two specific folders show up in their Explorer window with names that didn't match the file system name.

    When you ask for more information, explain what you need the information for, or at least be more specific what kind of "more information" you need. That way, you save everybody lots of time. The people answering your question don't waste their time gathering information you don't need (and gathering that information can be quite time-consuming), and you don't waste your time sifting through all the information you don't want.

    You might say that these people are employing the for-if anti-pattern:

    foreach (document d in GetAllPossibleDocumentation())
    {
     if (d.Topic == "password encryption on the wire") return d;
    }
    
  • The Old New Thing

    The story of the mysterious WINA20.386 file

    • 35 Comments

    matushorvath was curious about the WINA20.386 file that came with some versions of MS-DOS.

    The WINA20.386 file predates my involvement, but I was able to find some information on the Internet that explained what it was for. And it's right there in KB article Q68655: Windows 3.0 Enhanced Mode Requires WINA20.386:

    Windows 3.0 Enhanced Mode Requires WINA20.386

    Windows 3.0 enhanced mode uses a modular architecture based on what are called virtual device drivers, or VxDs. VxDs allow pieces of Windows to be replaced to add additional functionality. WINA20.386 is such a VxD. (VxDs could be called "structured" patches for Windows.)

    Windows 3.0 enhanced mode considers the state of the A20 line to be the same in all MS-DOS virtual machines (VMs). When MS-DOS is loaded in the high memory area (HMA), this can cause the machine to stop responding (hang) because of MS-DOS controlling the A20 line. If one VM is running inside the MS-DOS kernel (in the HMA) and Windows task switches to another VM in which MS-DOS turns off A20, the machine hangs when switching back to the VM that is currently attempting to execute code in the HMA.

    WINA20.386 changes the way Windows 3.0 enhanced mode handles the A20 line so that Windows treats the A20 status as local to each VM, instead of global to all VMs. This corrects the problem.

    (At the time I wrote this, a certain popular Web search engine kicks up as the top hit for the exact phrase "Windows 3.0 Enhanced Mode Requires WINA20.386" a spam site that copies KB articles in order to drive traffic. Meanwhile, the actual KB article doesn't show up in the search results. Fortunately, Bing got it right.)

    That explanation is clearly written for a technical audience with deep knowledge of MS-DOS, Windows, and the High Memory Area. matushorvath suggested that "a more detailed explanation could be interesting." I don't know if it's interesting; to me, it's actually quite boring. But here goes.

    The A20 line is a signal on the address bus that specifies the contents of bit 20 of the linear address of memory being accessed. If you aren't familiar with the significance of the A20 line, this Wikipedia article provides the necessary background.

    The High Memory Area is a 64KB-sized block of memory (really, 64KB minus 16 bytes) that becomes accessible when the CPU is in 8086 mode but the A20 line is enabled. To free up conventional memory, large portions of MS-DOS relocate themselves into the HMA. When a program calls into MS-DOS, it really calls into a stub which enables the A20 line, calls the real function in the HMA, and then disables the A20 line before returning to the program. (The value of the HMA was discovered by my colleague who also discovered the fastest way to get out of virtual-8086 mode.)

    The issue is that by default, Windows treats all MS-DOS device drivers and MS-DOS itself as global. A change in one virtual machine affects all virtual machines. This is done for compatibility reasons; after all, those old 16-bit device drivers assume that they are running on a single-tasking operating system. If you were to run a separate copy of each driver in each virtual machine, each copy would try to talk to the same physical device, and bad things would happen because each copy assumed it was the only code that communicated with that device.

    Suppose MS-DOS device drivers were treated as local to each virtual machine. Suppose you had a device driver that controlled a traffic signal, and as we all know, one of the cardinal rules of traffic signals is that you never show green in both directions. The device driver has two variables: NorthSouthColor and EastWestColor, and initially both are set to Red. The copy of the device driver running in the first virtual machine decides to let traffic flow in the north/south direction, and it executes code like this:

    if (EastWestColor != Red) {
     SetEastWestColor(Red);
    }
    SetNorthSouthColor(Green);
    

    Since both variables are initially set to Red, this code sets the north/south lights to green.

    Meanwhile, the copy of the device driver in the second virtual machine wants to let traffic flow in the east/west direction:

    if (NorthSouthColor != Red) {
     SetNorthSouthColor(Red);
    }
    SetEastWestColor(Green);
    

    Since we have a separate copy of the device driver in each virtual machine, the changes made in the first virtual machine do not affect the values in the second virtual machine. The second virtual machine sees that both variables are set to Red, so it merely sets the east/west color to green.

    On the other hand, both of these device drivers are unwittingly controlling the same physical traffic light, and it just got told to set the lights in both directions to Green.

    Oops.

    Okay, so Windows defaults drivers to global. That way, you don't run into the double-bookkeeping problem. But this causes problems for the code which manages the A20 line:

    Consider a system with two virtual machines. The first one calls into MS-DOS. The MS-DOS dispatcher enables the A20 line and calls the real function, but before the function returns, the virtual machine gets pre-empted. The second virtual machine now runs, and it too calls into MS-DOS. The MS-DOS dispatcher in the second virtual machine enables the A20 line and calls into the real function, and after the function returns, the second virtual machine disables the A20 line and returns to its caller. The second virtual machine now gets pre-empted, and the first virtual machine resumes execution. Oops: It tries to resume execution in the HMA, but the HMA is no longer there because the second virtual machine disabled the A20 line!

    The WINA20.386 driver teaches Windows that the state of the A20 should be treated as a per-virtual-machine state rather than a global state. With this new information, the above scenario does not run into a problem because the changes to the A20 line made by one virtual machine have no effect on the A20 line in another virtual machine.

    matushorvath goes on to add, "I would be very interested in how Windows 3.0 found and loaded this file. It seems to me there must have been some magic happening, e.g. DOS somehow forcing the driver to be loaded by Windows."

    Yup, that's what happened, and there's nothing secret about it. When Windows starts up, it broadcasts an interrupt. TSRs and device drivers can listen for this interrupt and respond by specifying that Windows should load a custom driver or request that certain ranges of data should be treated as per-virtual-machine state rather than global state (known to the Windows virtual machine manager as instance data). MS-DOS itself listens for this interrupt, and when Windows sends out the "Does anybody have any special requests?" broadcast, MS-DOS responds, "Yeah, please load this WINA20.386 driver."

    So there you have it, the story of WINA20.386. Interesting or boring?

  • The Old New Thing

    What's the difference between Text Document, Text Document - MS-DOS Format, and Unicode Text Document?

    • 16 Comments

    Alasdair King asks why Wordpad has three formats, Text Document, Text Document - MS-DOS Format, and Unicode Text Document. "Isn't at least one redundant?"

    Recall that in Windows, three code pages have special status.

    1. Unicode (more specifically, UTF-16LE)
    2. CP_ACP, commonly known as the ANSI code page, although that is a misnomer
    3. CP_OEM, commonly known as the OEM code page, although that too is a misnomer.

    Three text file formats. Three encodings. Hm... I wonder...

    As you might have guessed by now, the three text file formats correspond to the three special code pages. Now it's just a matter of deciding which one matches with which. The easiest one is the Unicode one; it seems clear that Unicode Text Document matches with Unicode. Okay, we now have to figure out how Text Document and Text Document - MS-DOS Format map to CP_ACP and CP_OEM. But another piece of the puzzle is pretty clear, because MS-DOS used the so-called OEM code page. Therefore, by process of elimination, Text Document corresponds to CP_ACP.

    Now that we have puzzled out what the three text formats correspond to, we can address the question "Isn't at least one redundant?"

    Michael Kaplan explained that ACP and OEM are (usually) different. And neither is the same as Unicode. So in fact all three are (usually) different.

    In the United States, the so-called ANSI code page is code page 1252, the so-called OEM code page is code page 437, and Unicode is code page 1200. Here's the string résumé expressed in each of the three encodings.

    Description Encoding Code page
    (en-us)
    Bytes
    Text Document CP_ACP 1252 72 E9 73 75 6D E9
    Text Document - MS-DOS Format CP_OEM 437 72 82 73 75 6D 82
    Unicode Text Document UTF-16LE 1200 FF FE 72 00 E9 00 73 00
    75 00 6D 00 E9 00

    Three encodings, three different files. No redundancy.

  • The Old New Thing

    The awesome Valentine's Day gift disguised as an uncreative one

    • 37 Comments

    A few years ago, one of my colleagues wanted to surprise his wife with a new laptop for Valentine's Day. (As a bonus, he set the wallpaper to one of their wedding pictures.) Now, he could just give her a neatly wrapped laptop, but he wanted this one to be a super-surprise.

    First, he bought a large box of chocolates. He then carefully opened the box (preserving the bow and other wrapping), removed the chocolates and put the laptop inside, using a smaller box of chocolates to act as packing material. He then put the cover back on the box of chocolates and restored the box to its original unopened appearance.

    As a final step, he took the completed package to a local grocery store, explained what he was doing to the deli manager, and asked if they would be so kind as to re-wrap the box in shrink wrap to complete the deception. The manager was suitably touched by his story and was happy to help.

    On Valentine's Day morning, he put the large box of chocolates on his wife's chair. She woke up, wandered groggily into the room, saw the box, and said, "Whoa, that's a lot of chocolate." It took some encouragement to get her to open the box (seeing as she hadn't had her morning cup of coffee yet), but when she did and saw the laptop, she just stared at it in shock, saying, "What? ... No, what?"

    In case you couldn't figure it out, his wife was taken totally by surprise and was completely thrilled.

    And that's how my colleague surprised his wife with a new laptop for Valentine's Day. He makes the rest of us look bad.

    Related: iPad frozen into slab of chocolate, delivered to unsuspecting wife.

    Bonus: The story from his wife's point of view.

  • The Old New Thing

    The path-searching algorithm is not a backtracking algorithm

    • 36 Comments

    Suppose your PATH environment variable looks like this:

    C:\dir1;\\server\share;C:\dir2
    

    Suppose that you call LoadLibrary("foo.dll") intending to load the library at C:\dir2\foo.dll. If the network server is down, the LoadLibrary call will fail. Why doesn't it just skip the bad directory in the PATH and continue searching?

    Suppose the LoadLibrary function skipped the bad network directory and kept searching. Suppose that the code which called LoadLibrary("foo.dll") was really after the file \\server\share\foo.dll. By taking the server down, you have tricked the LoadLibrary function into loading c:\dir2\foo.dll instead. (And maybe that was your DLL planting attack: If you can convince the system to reject all the versions on the PATH by some means, you can then get Load­Library to look in the current directory, which is where you put your attack version of foo.dll.)

    This can manifest itself in very strange ways if the two copies of foo.dll are not identical, because the program is now running with a version of foo.dll it was not designed to use. "My program works okay during the day, but it starts returning bad data when I try to run between midnight and 3am." Reason: The server is taken down for maintenance every night, so the program ends up running with the version in c:\dir2\foo.dll, which happens to be an incompatible version of the file.

    When the LoadLibrary function is unable to contact \\server\share\foo.dll, it doesn't know whether it's in the "don't worry, I wasn't expecting the file to be there anyway" case or in the "I was hoping to get that version of the file, don't substitute any bogus ones" case. So it plays it safe and assumes it's in the "don't substitute any bogus ones" and fails the call. The program can then perform whatever recovery it deems appropriate when it cannot load its precious foo.dll file.

    Now consider the case where there is also a c:\dir1\foo.dll file, but it's corrupted. If you do a LoadLibrary("foo.dll"), the call will fail with the error ERROR_BAD_EXE_FORMAT because it found the C:\dir1\foo.dll file, determined that it was corrupted, and gave up. It doesn't continue searching the path for a better version. The path-searching algorithm is not a backtracking algorithm. Once a file is found, the algorithm commits to trying to load that file (a "cut" in logic programming parlance), and if it fails, it doesn't backtrack and return to a previous state to try something else.

    Discussion: Why does the LoadLibrary search algorithm continue if an invalid directory or drive letter is put on the PATH?

    Vaguely related chatter: No backtracking, Part One

  • The Old New Thing

    Why does Windows keep showing the old indirect strings even after I update the binary?

    • 30 Comments

    If your application uses indirect localized string resources, and you update the application, you may find that Windows keeps using the old string from the previous version of the application.

    For example, suppose that you set the localized name for a shortcut to @C:\Program Files\Contoso\Contoso.exe,-1, and in version 1 of your program, you have

    LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
    STRINGTABLE
    BEGIN
    1 "Contoso Document Services"
    END
    
    LANGUAGE LANG_GERMAN, SUBLANG_NEUTRAL
    STRINGTABLE
    BEGIN
    1 "Contoso Dokumentdienste"
    END
    

    For version 2, your marketing team decides that the program should really be called Contoso Document System, so you change the resource file to read

    LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
    STRINGTABLE
    BEGIN
    1 "Contoso Document System"
    END
    
    LANGUAGE LANG_GERMAN, SUBLANG_NEUTRAL
    STRINGTABLE
    BEGIN
    1 "Contoso Dokumentsystem"
    END
    

    The user upgrades to version 2 of your program, but the shortcut on the Start menu still reads Contoso Document Services. What's going on?

    The shell keeps a cache of indirect localized strings because loading a DLL just to read a string out of it is pretty expensive. This cache is keyed by the string location specifier, and since your string location specifier hasn't changed from its previous value of @C:\Program Files\Contoso\Contoso.exe,-1, the shell continues using the value it stored away in its cache, which if the user had previously been using version 1 of your program, is the string Contoso Document Services.

    Some people, having discovered this behavior, have tried to go in and tinker with the shell's internal cache of indirect localized strings, but such a technique is doomed to failure because the location of that cache changes pretty regularly, and besides, it's an internal implementation detail. (And even if you find it and manage to fiddle with it, you only fix the problem for the current user. Other users will still have the stale cache entry.)

    The best solution is to treat indirect strings as locked: Once you decide what a string should say, you can't change it. When you issue version 2 of Contoso.exe, you can create a second string

    LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
    STRINGTABLE
    BEGIN
    1 "Contoso Document Services" // shortcuts from version 1.0 use this
    2 "Contoso Document System" // shortcuts from version 2.0 use this
    END
    
    LANGUAGE LANG_GERMAN, SUBLANG_NEUTRAL
    STRINGTABLE
    BEGIN
    1 "Contoso Dokumentdienste" // shortcuts from version 1.0 use this
    2 "Contoso Dokumentsystem" // shortcuts from version 2.0 use this
    END
    

    and have the installer for version 2.0 create a shortcut whose indirect localized string specifier is

    @C:\Program Files\Contoso\Contoso.exe,-2
    

    I admit that this method is rather clumsy and requires more attention on the part of the developer. Everybody wants the "cheap" way out, where the definition of "cheap" is not "cheapest for the customer" but rather "cheapest for me, the developer, because there's a new episode of Doctor Who tonight and I don't want to miss it."

    We saw last time that the format for indirect localized string resources has room for a comment. And it's the comment that we can take advantage of here. The shell uses the entire string location specifier as the key for its cache lookup, and that string includes the comment. Therefore, if you simply change the comment, that results in a cache miss, and the shell will go and re-fetch the string.

    @C:\Program Files\Contoso\Contoso.exe,-1;v2
    

    By appending a ;v2 to the string, you made it different from its predecessor, which means that the string cached by the predecessor won't be used.

    As I noted, this is cheap for the developer, but not necessarily cheap for the customer. Suppose the customer copied the shortcut to Contoso version 1 to their desktop, then upgraded to version 2. The upgrade replaces the shortcut in the Start menu, but the copy on the desktop remains unchanged. You now have a shortcut on the desktop whose indirect string is

    @C:\Program Files\Contoso\Contoso.exe,-1
    

    and a shortcut on the Start menu whose indirect string is

    @C:\Program Files\Contoso\Contoso.exe,-1;v2
    

    Since the shortcut on the desktop was created while version 1 was still installed on the computer, its name will read Contoso Document Services because that was the contents of string 1. On the other hand, the shortcut on the Start menu will read Contoso Document System because its use of the ;v2 forced the shell to go back and look again, and this time it sees the revised string. So far so good.

    But then the user does something which causes the cache to be pruned, like, say, changing their UI language to German. The shell says, "Okay, the UI language changed, I need to go reload all these indirect strings because MUI is going to change them to the new language." The shell sees the shortcut on the Start menu, reads string 1 out of Contoso.exe, and gets Contoso Dokumentsystem. The shell then sees the shortcut on the desktop, reads string 1 out of Contoso.exe, and gets... Contoso Dokumentsystem. Not Contoso Dokumentdienste.

    Notice that the name of the shortcut on the desktop was silently upgraded to Contoso version 2.

    Even if the user changes the language back to English in an attempt to get things back to the way they were, it won't work. The shell sees the shortcut on the Start menu, reads string 1 out of Contoso.exe, and gets Contoso Document System. The shell then sees the shortcut on the desktop, reads string 1 out of Contoso.exe, and gets Contoso Document System, not Contoso Document Service. The original string from the first version of Contoso.exe is already gone; the only way to get it back is to reinstall Contoso version 1.

    But at least you didn't miss your TV show.

    Bonus chatter: The one case I can think of where the cheap way out is acceptable is when you are issuing a prerelease version. For your prerelease versions, you can append ;prerelease build xxxxx to your string location specifier (where xxxxx is the build number), so that each time the user upgrades to a new build, the string is reloaded from scratch. This still has the same problem described above if the user has data left over from a previous build, but since it's a prerelease build, you can just declare that as not a supported configuration.

  • The Old New Thing

    What does the minus sign in indirect localized string resources mean?

    • 7 Comments

    The syntax for indirect localized string resources is @filename,-stringId, optionally followed by a semicolon and a comment. A customer wanted to know what the minus signs stands for.

    The minus sign doesn't "stand for" anything. It's just part of the syntax. It's like asking what the semicolon at the end of a C statement stands for. It doesn't stand for anything; it's just part of the rules for C statements. (And if the minus sign has to stand for something, what does the comma stand for?)

    Okay, so maybe the question was really "Why does the syntax for indirect localized strings include a minus sign? Isn't the comma enough?"

    From a parsing standpoint, the comma is enough. The syntax for indirect strings was influenced by the syntax for icon locations, which also takes the form filename,number. We saw some time ago that the number after the comma can be positive or negative or zero. If positive or zero, it specifies the zero-based icon index. If negative, then it specifies the (negative of the) icon ID.

    The indirect string syntax follows the same pattern, except that they don't support string indices. (As we saw earlier when we studied the format of string resources, a null string is indistinguishable from no string at all, which makes string indices largely meaningless since you can't tell whether a null string should be counted towards the index or not.) Since the only thing supported is IDs, and IDs are expressed as negative values, the first thing after the comma is always a minus sign.

    Next time, we'll take a closer look at that comment field.

Page 1 of 3 (21 items) 123