April, 2014

  • The Old New Thing

    The StartRunNoHOMEPATH policy affects whether the user's HOME path is the current directory when you use Start.Run, but make sure you read that carefully

    • 15 Comments

    A customer (via the customer liaison) discovered that even though they had set the Start­Run­No­HOME­PATH policy, they found "if the user creates a Notepad file, the file is searched in the user's HOME directory, in contradiction of Start­Run­No­HOME­PATH policy,"

    I asked the liaison to confirm: "The steps you describe are rather vague. Are you saying that the problem occurs when the user opens the Run dialog and types Notepad filename.txt?"

    The customer liaison replied, "I believe the scenario is close to what you describe. The user opens the Run dialog, types Notepad, then types some text into Notepad and then does a Save As. I will confirm with the customer."

    A few days later (the customer was on leave), the customer liaison provided the exact steps:

    • Open the Start menu (Windows 7)
    • Type Notepad to search the Start menu.
    • When the Notepad program is found, click on it.
    • Type some text.
    • Perform a Save As. This operation is slow. Network traces show many accesses to the user's HOME directory.

    Okay, well, now that the steps are all carefully spelled out, it is clear what is going on. Or more accurately, it is clear what is not going on.

    The Start­Run­No­HOME­PATH policy controls the working directory when a program is run from the Start.Run dialog. Like it says in the KB article:

    Symptoms

    If you have a home folder set and you try to run a program by clicking Start and then clicking Run, Windows searches your home folder for the program before searching the path.

    The article then goes on to describe how you can solve the problem if those are the symptoms you are trying to relieve.

    But those symptoms do not match the customer's problem.

    The customer ran the program directly from the Start menu, not by going through the Start.Run dialog. Therefore, the KB article and the Start­Run­No­HOME­PATH policy do not apply.

    Policies do what they are documented to do, not what you wish they did.

  • The Old New Thing

    Why does the common file save dialog create a temporary file and then delete it?

    • 29 Comments

    When you call GetSaveFileName, the common file save dialog will ask the user to choose a file name, and just before it returns it does a little create/delete dance where it creates the file the user entered, and then deletes it. What's up with that?

    This is a leftover from the ancient days of 16-bit Windows 3.1, back when file systems were real file systems and didn't have this namby-pamby "long file name" or "security" nonsense. (Insert sound effect of muscle flexing and gutteral grunting.)

    Back in those days, the file system interface was MS-DOS, and MS-DOS didn't have a way to query security attributes because, well, the file systems of the day didn't have security attributes to query in the first place.

    But network servers did.

    If you mapped a network drive from a server running one of those fancy new file systems, then you were in this case where your computer didn't know anything about file system security, but the server did. The only way to find out whether you had permission to create a file in a directory was to try it and see whether it worked or whether it failed with the error ERROR_ACCESS_DENIED (or, as it was called back in the MS-DOS days, "5"),

    Another reason why a server might reject a file name was that it contained a character that, while legal in Windows, was not legal on the server. At the time, the most common reason for this was that you used a so-called "extended character" (in other words, a character outside the ASCII range like an accented lowercase e) which was part of your local code page but not on the server's.

    Yet another possibility was that the file name you chose would exceed the server's path name limit. For example, suppose the server is running Windows for Workgroups (which has a 64-character maximum path name limit), and it shared out C:\some\deep\directory\on\the\server as \\server\share. If you mapped M: to \\server\share, then the maximum path name on M: was only about 30 characters because C:\some\deep\directory\on\the\server used up half of your 64-character limit.

    The only way to tell whether the file could be created, then, was to try to create it and see what happens. After creating the test file (to see if it could), the common file save dialog immediately deleted it in order to cover its tracks. (This could lead to some weird behavior if users picked a directory where they had permission to create files but no permission to delete files that they created!)

    This "test to see if I can create the file by creating it" behavior has been carried forward ever since, but you can suppress it by passing the OFN_NOTESTFILECREATE flag.

  • The Old New Thing

    Showing a balloon tip at a specific position, then removing it

    • 5 Comments

    Today's Little Program shows a balloon tip at a specific position, then manually removes it.

    Start with our scratch program and make these changes:

    #pragma comment(linker, \
        "\"/manifestdependency:type='Win32' "\
        "name='Microsoft.Windows.Common-Controls' "\
        "version='6.0.0.0' "\
        "processorArchitecture='*' "\
        "publicKeyToken='6595b64144ccf1df' "\
        "language='*'\"")
    
    HWND g_hwndTT;
    TOOLINFO g_ti;
    
    BOOL
    OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
    {
      g_hwndTT = CreateWindow(TOOLTIPS_CLASS, nullptr,
                WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
                0, 0, 0, 0, hwnd, nullptr, g_hinst, nullptr);
      g_ti.uFlags = TTF_TRACK;
      g_ti.hwnd = hwnd;
      g_ti.lpszText = TEXT("Hi there");
      SendMessage(g_hwndTT, TTM_ADDTOOL, 0, (LPARAM)&g_ti);
    
      return TRUE;
    }
    
    void OnChar(HWND hwnd, TCHAR ch, int cRepeat)
    {
      POINT pt;
      switch (ch) {
      case TEXT(' '):
        if (GetCursorPos(&pt)) {
          SendMessage(g_hwndTT, TTM_TRACKPOSITION, 0, MAKELPARAM(pt.x, pt.y));
          SendMessage(g_hwndTT, TTM_TRACKACTIVATE, TRUE, (LPARAM)&g_ti);
        }
        break;
      case 27: // ESCAPE
        SendMessage(g_hwndTT, TTM_TRACKACTIVATE, FALSE, 0);
        break;
      }
    }
    
      HANDLE_MESSAGE(hwnd, WM_CHAR, OnChar);
    

    When our main window is created, we also create a balloon-style tooltip and add a tracking tool. Normally, the tooltip control appears and disappears automatically, at a position of the tooltip's choosing. Tracking tooltips are managed manually, so you can specify exactly when and where they appear, and you also manually remove them from the screen. At startup, we add the tool but do not show the balloon tooltip yet.

    When the user presses the space bar, we get the current cursor position and tell the tracking tooltip to appear at exactly that location, then we activate tracking mode. The result: The balloon tip appears, and the tip of the balloon points directly at the mouse cursor.

    When the user presses the ESC key, we deactivate tracking mode, which removes the tooltip from the screen.

  • The Old New Thing

    A discovered quirk is just few steps away from becoming a feature

    • 24 Comments

    Commenter Cherry wonders who invented all those strange syntaxes, like set " to show all environment variables, including the hidden ones.

    An interesting historical note is the origin of the convention in unix that files whose names begin with a dot are hidden by default (here's the relevant portion). That article highlights how a discovered quirk is just a few steps away from becoming a feature.

    As Master Yoda might put it: Discovery leads to dissemination. Dissemination leads to adoption. Adoption leads to entrenchment. Entrenchment creates a compatibility constraint.

    As I've noted many times, the batch language was not designed. It simply evolved out of the old CP/M program SUBMIT, which was an even more rudimentary batch processor. (The original SUBMIT.COM didn't have conditional branches. It merely ran every line in your batch file one after another.)

    One of the consequences of something that old is that any quirk, once discovered, can turn into a feature, and from there it becomes a support burden and compatibility constraint. We've seen this many times before: Counting the number of lines in a file by exploiting a buffer underflow bug in FIND.COM. Update the last-modified time of a file by using a magic sequence of punctuation marks. Echoing a blank line by typing ECHO.. All of these were accidental discovered behaviors (just like unix dot files) which became entrenched. Even when the underlying program was completely rewritten, these special quirks had to be specifically detected and painstakingly reproduced because so many programs (i.e., batch files) relied on them.

    For set ", it's a case of taking advantage of two quirks in the implementation: The first quirk is that a missing close-quotation mark is forgiven. That means that set " is logically equivalent to set "".

    You are therefore asking for a filtered list of environment variables, but passing the logical equivalent of no filter. Specifically, you're asking for all environment variables which begin with the empty string, and it so happens that every string begins with the empty string. The second quirk is that when an explicit filter is applied, the set command disables its default filter of "Hide environment variables whose names begin with an equals sign."

    In other words, the code goes like this:

    foreach (var entry in Environment.GetEnvironmentVariables()) {
     if (prefixFilter != null ?
         entry.Key.StartsWith(prefixFilter) :
         !entry.Key.StartsWith("=")) {
      Console.WriteLine("{0}={1}", entry.Key, entry.Value);
     }
    }
    

    Perhaps this is a bug, and it should have been written like this:

    foreach (var entry in Environment.GetEnvironmentVariables()) {
     if (!entry.Key.StartsWith("=") &&
         (prefixFilter == null || entry.Key.StartsWith(prefixFilter))) {
      Console.WriteLine("{0}={0}", entry.Key, entry.Value);
     }
    }
    

    But it's too late to fix it now. People have discovered the quote trick, so it's now a feature and therefore a compatibility constraint.

  • The Old New Thing

    I thought you could use SWP_FRAMECHANGED to force a control to recalculate its properties after a change in styles

    • 7 Comments

    Simon Cooke dug back into his memory and asked, "Doesn't calling Set­Window­Pos with SWP_FRAME­CHANGED cause a recreate and re-apply of the styles?"

    The SWP_FRAME­CHANGED flag does not recreate anything, but it does reapply the styles, as far as it knows.

    Recall that the bits in the window style break into two parts. There are the styles managed by the window manager, which are in the upper 16 bits, and there are the styles that are specific to each control, which are in the lower 16 bits.

    The window manager knows about the styles that it manages, but it has no clue about the styles that are specific to each control. It has no idea that the MCIWNDF_NO­PLAY­BAR style controls the toolbar in an MCI window, or that the ES_RIGHT style controls the alignment of text in an edit control.

    The SWP_FRAME­CHANGED flag tells the window manager, "Hey, I changed some styles that affect the non-client area of the window (the window frame). Could you go and re-read those styles and apply them to the window? Thanks." That's sort of implied in the name: "Frame changed."

    If you want a control to re-inspect the window styles and adjust its behavior in response, you need to do something control-specific. The control might have a custom message you can send it to say, "Hey, I changed some styles that afect the client area of the window. Could you go and re-read those styles and apply them to the window? Thanks." Or there may be special messages specifically for changing styles, such as EM_SET­READ­ONLY. The fancier windows may do it automatically on receipt of the WM_STYLE­CHANGED messages.

  • The Old New Thing

    How do I programmatically create folders like My Pictures if they were manually deleted?

    • 10 Comments

    A corporate customer had a problem with their employees accidentally deleting folders like Videos and Pictures and are looking for a way to restore them, short of blowing away the entire user profile and starting over. They found some techniques on the Internet but they don't always work consistently or completely. What is the recommended way of recreating these missing folders?

    It turns out that the customer was asking a question that I answered many years ago, but looking at it from the other side.

    To recreate a folder, call SHGet­Folder­Path with the flag CSIDL_FLAG_CREATE, or call SHGet­Special­Folder­Path and pass fCreate = TRUE.

    If you are targeting Windows Vista or higher, the known-folder equivalent is calling SHGet­Known­Folder­Path, SHGet­Known­Folder­ID­List, or SHGet­Known­Folder­Item with the KF_FLAG_CREATE flag.

    (There is a CSIDL-to-KF conversion table in MSDN.)

  • The Old New Thing

    Le Chatelier's Principle in action: Administrative overrides

    • 43 Comments

    Today we have another example of Le Chatelier's Principle as interpreted by John Gall: Every system resists its proper functioning.

    There was a video card manufacturer which was using the AppInit_DLLs key so that they could inject their DLL into every process. I have no idea why. Perhaps to get a nice bonus.

    In Windows Vista, the AppInit_DLLs registry key was deactivated for both engineering and security reasons. Oh no! Undeterred, the video card manufacturer issued an update to their driver so that in addition to adding themselves to AppInit_DLLs, they also set the administrative override switch that re-enabled the feature. Boom, they probably got a second bonus for that.

    Another lesson from this story is that if you provide an administrative override to restore earlier behavior, then you never really removed the earlier behavior. Since installers run with administrator privileges, they can go ahead and flip the setting that is intended to be set only by system administrators.

  • The Old New Thing

    How can I get information about the items in the Recycle Bin from script?

    • 7 Comments

    Today we'll do a scripting version of an old C++ program: Printing information about the items in the Recycle Bin. (How you wish to act on the information is up to you.)

    This is a pattern we've seen a lot. Bind to a folder, enumerate its contents, extract properties.

    var shell = new ActiveXObject("Shell.Application");
    var recycleBin = shell.NameSpace(10); // CSIDL_BITBUCKET
    var items = recycleBin.Items();
    for (var i = 0; i < items.Count; i++) {
      var item = items.Item(i);
      WScript.StdOut.WriteLine(item.Name);
      WScript.StdOut.WriteLine(item.ExtendedProperty(
                                    "System.Recycle.DeletedFrom"));
      WScript.StdOut.WriteLine(item.ExtendedProperty(
                                    "System.Recycle.DateDeleted"));
      WScript.StdOut.WriteLine(item.Size);
    }
    

    Wow, that was way easier than doing it in C++!

    Just for fun, I'll do it in C#, first as a straight port:

    // add a reference to shell32.dll
    class Program {
      public static void Main()
      {
        var shell = new Shell32.Shell();
        var recycleBin = shell.NameSpace(10); // CSIDL_BITBUCKET
        var items = recycleBin.Items();
        for (var i = 0; i < items.Count; i++) {
          var item = (Shell32.FolderItem2)items.Item(i);
          System.Console.WriteLine(item.Name);
          System.Console.WriteLine(item.ExtendedProperty(
                                        "System.Recycle.DeletedFrom"));
          System.Console.WriteLine(item.ExtendedProperty(
                                        "System.Recycle.DateDeleted"));
          System.Console.WriteLine(item.Size);
        }
      }
    }
    

    We have to cast to Shell32.Folder­Item2 because the default interface for the Item() method is Shell32.Folder­Item, but Extended­Property is a method on Shell32.Folder­Item2. We didn't have to do this explicit cast in JavaScript because JavaScript is a dynamically-typed language.

    So let's use the dynamic keyword to mimic that in C#. Note, however, that if you use dynamic, then you miss out on a lot of IntelliSense features.

    class Program {
      public static void Main()
      {
        var shell = new Shell32.Shell();
        var recycleBin = shell.NameSpace(10); // CSIDL_BITBUCKET
        var items = recycleBin.Items();
        foreach (dynamic item in items) {
          System.Console.WriteLine(item.Name);
          System.Console.WriteLine(item.ExtendedProperty(
                                        "System.Recycle.DeletedFrom"));
          System.Console.WriteLine(item.ExtendedProperty(
                                        "System.Recycle.DateDeleted"));
          System.Console.WriteLine(item.Size);
        }
      }
    }
    

    Now you can do things like list all the files deleted today

    class Program {
      public static void Main()
      {
        var today = DateTime.Today;
        var shell = new Shell32.Shell();
        var recycleBin = shell.NameSpace(10); // CSIDL_BITBUCKET
        var items = recycleBin.Items();
        foreach (dynamic item in items) {
          if (item.ExtendedProperty("System.Recycle.DateDeleted").Date
                                                           == today) {
            System.Console.WriteLine(item.name);
          }
        }
      }
    }
    
  • The Old New Thing

    Raymond's house rules for Easter Egg Hunts

    • 19 Comments

    One of my colleagues frustrates his family by hiding the eggs for the annual Egg Hunt way too well. "Apparently, drawers and freezers are out of bounds in the traditional egg hunt."

    Here are my house rules for Easter Egg Hunts:

    • All eggs must be hidden within the implied egg-hiding area. No sneaky outliers.
    • All eggs must be at least partially observable by egg-hunters without disturbing anything. No hiding in drawers or under flowerpots, or putting them on top of a tall piece of furniture that a shorter egg-hunter cannot see.
    • However, you may still have to work to see them. They might be behind a sofa or placed above eye level. For example, you might find an egg tucked between the slats of horizontal blinds.

    Personally, I like to hide eggs in plain sight. It's surprising how long it can take somebody to find a yellow egg resting brazenly on the lap of a yellow teddy bear.

  • The Old New Thing

    How do I set a breakpoint on a function whose name contains spaces or other special characters?

    • 3 Comments

    If you use one of the command line debuggers based on the Debugger Engine, you can set a breakpoint on a function whose name contains spaces or other special characters by quoting the symbol name. The trick here is that you do not quote the entire string; you quote only the symbol name.

    0:001> bu @!"CSimpleArray<wchar_t *>::CSimpleArray<wchar_t *>"
    

    Note that the quotation marks do not go around the @! part. They go only around the symbol. (Otherwise, the debugger thinks you are setting a breakpoint action.)

    Another trick for setting breakpoints is using tab autocompletion for symbols. If you type bp contoso!*Widget* and then hit Tab repeatedly, you will cycle through all the matches. (It takes a few seconds to build the list of matches, so be patient the first time you hit Tab.)

    Personally, I use the x command to print out all the matches, and then cherry-pick the one I want.

    0:001> x contoso!*Widget*
    00af114c contoso!CreateWidget
    009fe863 contoso!DestroyWidget
    00a2e161 contoso!MakeWidgetReadOnly
    00a93168 ...
    
    0:001> bp 00a2e161     set breakpoint on MakeWidgetReadOnly
    
Page 1 of 3 (25 items) 123