October, 2011

  • The Old New Thing

    How do I access a file without updating its last-access time?

    • 29 Comments

    The first problem with discussing file last-access time is agreeing what you mean by a file's last-access time.

    The file system folks have one definition of the file last-access time, namely the time the file was most recently opened and either read from or written to. This is the value retrieved by functions like Get­File­Attributes­Ex, Get­File­Time, and Find­First­File.

    The problem with this definition is that it doesn't match the intuitive definition of last-access time, which is "the last time I accessed the file," emphasis on the I. In fact, the intuitive definition of access is more specific: It's "the last time I opened, modified, printed, or otherwise performed some sort of purposeful action on the file."

    This discrepancy between the file system definition and the intuitive definition means that a lot of operations trigger a file system access but shouldn't count as an access from the user interface point of view. Here are some examples:

    Whenever some shell extension violates this rule, the shell team gets a bug report from some customer saying, "The last-access time shown in Explorer is wrong. A document which hasn't been accessed in months shows a last-access time of today. After closer investigation, we found that the last-access time updates whenever we insert seemingly-innocuous operation here."

    If you're writing a program which needs to access the file contents but you not want to update the last-access time, you can use the Set­File­Time function with the special value 0xFFFFFFFF in both members of the FILETIME structure passed as the last-access time. This magic value means "do not change the last-access time even though I'm accessing the file."

    BOOL DoNotUpdateLastAccessTime(HANDLE hFile)
    {
     static const FILETIME ftLeaveUnchanged = { 0xFFFFFFFF, 0xFFFFFFFF };
     return SetFileTime(hFile, NULL, &ftLeaveUnchanged, NULL);
    }
    

    As the documentation notes, you have to call this function immediately after opening the file.

    Going back to that linked comment: The reason why viewing the Summary tab causes the last-access time to be updated is that the Summary tab retrieves its information by calling Stg­Open­Storage, and there's no way to tell that function, "Hey, when you open the file in order to see if it has any document properties, do that Do­Not­Update­Last­Access­Time thing so you don't update the last access time."

    Bonus chatter: Starting in Windows Vista, maintaining the last-access time is disabled by default. In practice, this means that the number of bugs related to altering the last-access time accidentally will multiply unchecked, because the mechanism for detecting the error is disabled by default.

  • The Old New Thing

    During process termination, slim reader/writer locks are now also electrified

    • 10 Comments

    Some time ago I mentioned that during process termination, the gates are now electrified: If you attempt to enter a critical section that is owned by a thread that was terminated by an earlier phase of process termination, the entire process is forcibly terminated.

    Windows Vista introduced a new lightweight synchronization pseudo-object known as the slim reader/writer lock. And if you tried to enter a slim reader/writer lock during process termination and found yourself waiting for the current owner to release it, you ended up waiting forever since the current owner was terminated by an earlier phase of process termination. The sentence "As for the home-grown stuff, well, you're on your own" applies here. Even though the slim reader/writer lock functions are exported from kernel32.dll, they don't have any special kernel powers with respect to process termination. From the standpoint of process termination, they may as well be some home-grown synchronization primitive.

    In Windows 7, the kernel folks decided to bring slim reader/writer locks into the fold of objects which are electrified during process termination. Starting in Windows 7, if you attempt to acquire a slim reader/writer lock during process termination, and the lock cannot be immediately acquired, then the process is forcibly terminated.

  • The Old New Thing

    Why can't I move the Program Files directory via the unattend file?

    • 53 Comments

    We saw last time that the unattend file lets you change some Windows configuration settings that cannot be changed after Setup is complete. But one of the things you can't change is the location of the Program Files directory. Many people wish they could relocate their Program Files directory to another drive in order to relieve disk space pressure on the system partition. Why won't Windows let them do this?

    Now that NTFS is mandatory for the system volume (it took only 13 years to get there!), Windows itself can start taking advantage of NTFS features.

    For example, Windows Update can take advantage of transactions: When updates are applied, they are done as part of a transaction. That way, if something horrific happens in the middle of an update, the entire transaction becomes abandoned and you don't get stuck with some Frankenstein configuration.

    Windows Setup takes advantage of hard links. A large percentage of the files installed by Windows are hard-linked to copies in the C:\Windows\WinSxS directory for reasons I do not understand, but the phrase "component store" may be part of it. (This is why asking Explorer for the size of the C:\Windows directory gives a misleading view of the actual amount of disk space occupied by Windows, because Explorer uses a naive algorithm which counts each hard link as a separate file.) Oh, and in Windows 7, the two copies of Notepad are now hard links to each other.

    Ah, but one of the limitations of hard links is that they cannot span volumes. Some of the hard links out of the WinSxS directory point into places like C:\Program Files\Windows NT\Accessories\wordpad.exe, and this in turn requires that the Program Files directory be on the same volume as your Windows directory.

    Sorry for the inconvenience.

  • The Old New Thing

    The unattend file lets you configure Windows while it's installing, and in some cases it's your only chance

    • 32 Comments

    Some Windows settings can only be established as part of the installation process. This is done with a so-called unattend file. (Remember, no matter where you put an advanced setting, somebody will tell you that you are an idiot.) In earlier versions of Windows, the unattend file took the form of an INI file, but Windows Vista hopped aboard the XML bandwagon, and the unattend file format changed to XML. The nice thing about using XML is that you can publish a schema so people can validate their unattend file without having to perform a test install (only to discover twenty minutes later that a typo resulted in an entire section of the unattend file being ignored, say).

    If you spend a lot of time setting up computers, you can use an unattend file to answer all the Setup questions (like "enter your product key") so all you have to do is type "setup /unattend:myconfiguration.xml" and go out to lunch. When you come back, your machine will be installed and ready.

    Here are two of the most popular unattend settings which must be set during installation. (There are a bunch of popular unattend settings for things that can also be changed post-install; for those other settings, the unattend file is not your only chance.)

    Wait, the C:\Program Files directory isn't on the list of directories that can be relocated. There's a reason for that, which we'll look at next time.

  • The Old New Thing

    Beyoncé, the giant metal chicken has a Facebook page

    • 4 Comments

    In my 2011 mid-year link clearance, I linked to the story And that's why you should learn to pick your battles. I thought that was the end of the story, but no, it's the gift that keeps on giving. Beyoncé, the giant metal chicken has a Facebook page, populated with all sorts of crazy things like pictures of Beyoncé's relatives spotted in the wild (some of them knocking on doors or peeking in windows), a No Soliciting sign just for giant metal chickens, and an updated version of the chart of anniversary gifts which lists BIG METAL CHICKEN as the modern 15th anniversary present.

  • The Old New Thing

    Adjusting your commute to avoid being at work quite so much

    • 3 Comments

    Commenter Bernard remarked that he moved from one side of the company campus to the other, and his commute distance (and time) was cut in half.

    That reminds me of a short story from a now-retired colleague of mine. He bicycled to work, and over the years, the gradual expansion of the Microsoft main corporate campus resulted in nearly three quarters of his commute to work taking place on Microsoft property. This bothered him to the point where he changed his route just so he wouldn't be "at work" practically the moment he left his house.

    Bonus chatter: Looks like Michael Kaplan also has one of these unbalanced commutes.

  • The Old New Thing

    Do not access the disk in your IContextMenu handler, no really, don't do it

    • 22 Comments

    We saw some time ago that the number one cause of crashes in Explorer is malware.

    It so happens that the number one cause of hangs in Explorer is disk access from context menu handlers (a special case of the more general principle, you can't open the file until the user tells you to open it).

    That's why I was amused by Memet's claim that "would hit the disk" is not acceptable for me. The feedback I see from customers, either directly from large multinational corporations with 500ms ping times or indirectly from individual users who collectively click Send Report millions of times a day, is that "would hit the disk" ruins a lot of people's days. It may not be acceptable to you, but millions of other people would beg to disagree.

    The Windows team tries very hard to identify unwanted disk accesses in Explorer and get rid of them. We don't get them all, but at least we try. But if the unwanted disk access is coming from a third-party add-on, there isn't much that can be done aside from saying, "Don't do that" and hoping the vendor listens.

    Every so often, a vendor will come back and ask for advice on avoiding disk access in their context menu handler. There's a lot of information packed into that data object that contains information gathered from when the disk was accessed originally. You can just retrieve that cached data instead of going off and hitting the disk again to recalculate it.

    I'm going to use a boring console application and the clipboard rather than building a full IContext­Menu, since the purpose here is to show how to get data from a data object without hitting the disk and not to delve into the details of IContext­Menu implementation.

    #define UNICODE
    #define _UNICODE
    #include <windows.h>
    #include <ole2.h>
    #include <shlobj.h>
    #include <propkey.h>
    #include <tchar.h>
    
    void ProcessDataObject(IDataObject *pdto)
    {
     ... to be written ...
    }
    
    int __cdecl _tmain(int argc, PTSTR *argv)
    {
     if (SUCCEEDED(OleInitialize(NULL))) {
      IDataObject *pdto;
      if (SUCCEEDED(OleGetClipboard(&pdto))) {
       ProcessDataObject(pdto);
       pdto->Release();
      }
      OleUninitialize();
     }
    }
    

    Okay, let's say that we want to check that all the items on the clipboard are files and not directories. The HDROP way of doing this would be to get the path to each of the items in the data object, then call Get­File­Attributes on each one to see if any of them has the FILE_ATTRIBUTE_DIRECTORY flag set. But this hits the disk, which makes baby context menu host sad. Fortunately, the IShell­Item­Array interface provides an easy way to check whether any or all the items in a data object have a particular attribute.

    void ProcessDataObject(IDataObject *pdto)
    {
     IShellItemArray *psia;
     HRESULT hr;
     hr = SHCreateShellItemArrayFromDataObject(pdto,
                                              IID_PPV_ARGS(&psia));
     if (SUCCEEDED(hr)) {
      SFGAOF sfgaoResult;
      hr = psia->GetAttributes(SIATTRIBFLAGS_OR, SFGAO_FOLDER,
                                                     &sfgaoResult);
      if (hr == S_OK) {
       _tprintf(TEXT("Contains a folder\n"));
      } else if (hr == S_FALSE) {
       _tprintf(TEXT("Contains no folders\n"));
      }
      psia->Release();
     }
    }
    

    In this case, we want to see if any item (SI­ATTRIB­FLAGS_OR) in the data object has the SFGAO_FOLDER attribute. The IShell­Item­Array::Get­Attributes method returns S_OK if all of the attributes you requested are present in the result. Since we asked for only one attribute, and since we asked for the result to be the logical or of the individual attributes, this means that it returns S_OK if any item is a folder.

    Okay, fine, but what if the thing you want to know is not expressible as a SFGAO flag? Well, you can dig into each of the individual items. For example, suppose we want to see the size of each item.

    #include <strsafe.h>
    
    void ProcessDroppedObject(IDataObject *pdto)
    {
     IShellItemArray *psia;
     HRESULT hr;
     hr = SHCreateShellItemArrayFromDataObject(pdto,
                                              IID_PPV_ARGS(&psia));
     if (SUCCEEDED(hr)) {
      IEnumShellItems *pesi;
      hr = psia->EnumItems(&pesi);
      if (SUCCEEDED(hr)) {
       IShellItem *psi;
       while (pesi->Next(1, &psi, NULL) == S_OK) {
        IShellItem2 *psi2;
        hr = psi->QueryInterface(IID_PPV_ARGS(&psi2));
        if (SUCCEEDED(hr)) {
         ULONGLONG ullSize;
         hr = psi2->GetUInt64(PKEY_Size, &ullSize);
         if (SUCCEEDED(hr)) {
          _tprintf(TEXT("Item size is %I64u\n"), ullSize);
         }
         psi2->Release();
        }
        psi->Release();
       }
      }
      psia->Release();
     }
    }
    

    I went for IEnum­Shell­Items here, even though a for loop with IShell­Item­Array::Get­Count and IShell­Item­Array::Get­Item­At would have worked, too.

    File system items in data objects cache a bunch of useful pieces of information, such as the last-modified time, file creation time, last-access time, the file size, the file attributes, and the file name (both long and short). Of course, all of these properties are subject to file system support. the shell just takes what's in the WIN32_FIND_DATA; if the values are incorrect (for example, if last-access time tracking is disabled), then the shell is going to cache the incorrect value. But don't say, "Well, if the cache is no good, then I won't use it; I'll just go hit the disk", because if you hit the disk, the file system is going to give you the same incorrect value anyway!

    If you just want to order the combo platter, you can ask for PKEY_Find­Data, and out will come a WIN32_FIND_DATA. This might be the easiest way to convert your old-style context menu that hits the disk into a new-style context menu that doesn't hit the disk: Take your calls to Get­File­Attributes and Find­First­File and convert them into calls into the property system, asking for PKEY_File­Attributes or PKEY_Find­Data.

    Okay, that's the convenient modern way to get information that has been cached in the data object provided by the shell. What if you're an old-school programmer? Then you get to roll up your sleeves and get your hands dirty with the CFSTR_SHELL­ID­LIST clipboard format. (And if your target is Windows XP or earlier, you have to do it this way since the IShell­Item­Array interface was not introduced until Windows Vista.) In fact, the CFSTR_SHELL­ID­LIST clipboard format will get your hands so dirty, I'm writing a helper class to manage it.

    First, go back and familiarize yourself with the CIDA structure.

    // these should look familiar
    #define HIDA_GetPIDLFolder(pida) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[0])
    #define HIDA_GetPIDLItem(pida, i) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[i+1])
    
    void ProcessDataObject(IDataObject *pdto)
    {
     FORMATETC fmte = {
        (CLIPFORMAT)RegisterClipboardFormat(CFSTR_SHELLIDLIST),
        NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
     STGMEDIUM stm = { 0 }; // defend against buggy data object
     HRESULT hr = pdto->GetData(&fmte, &stm);
     if (SUCCEEDED(hr) && stm.hGlobal != NULL) {
      LPIDA pida = (LPIDA)GlobalLock(stm.hGlobal);
      if (pida != NULL) { // defend against buggy data object
       IShellFolder *psfRoot;
       hr = SHBindToObject(NULL, HIDA_GetPIDLFolder(pida), NULL,
                           IID_PPV_ARGS(&psfRoot));
       if (SUCCEEDED(hr)) {
        for (UINT i = 0; i < pida->cidl; i++) {
         IShellFolder2 *psf2;
         PCUITEMID_CHILD pidl;
         hr = SHBindToFolderIDListParent(psfRoot,
                    HIDA_GetPIDLItem(pida, i),
                    IID_PPV_ARGS(&psf2), &pidl);
         if (SUCCEEDED(hr)) {
          VARIANT vt;
          if (SUCCEEDED(psf2->GetDetailsEx(pidl, &PKEY_Size, &vt))) {
           if (SUCCEEDED(VariantChangeType(&vt, &vt, 0, VT_UI8))) {
             _tprintf(TEXT("Item size is %I64u\n"), vt.ullVal);
           }
           VariantClear(&vt);
          }
          psf2->Release();
         }
        }
        psfRoot->Release();
       }
       GlobalUnlock(stm.hGlobal);
      }
      ReleaseStgMedium(&stm);
     }
    }
    

    I warned you it was going to be ugly.

    First, we retrieve the CFSTR_SHELL­ID­LIST clipboard format from the data object. This format takes the form of an HGLOBAL, which needs to be Global­Lock'd like all HGLOBALs returned by IData­Object::Get­Data. You may notice two defensive measures here. First, there is a defense against data objects which return success when they actually failed. To detect this case, we zero out the STG­MEDIUM and make sure they returned something non-NULL in it. The second defensive measure is against data objects which put an invalid HGLOBAL in the STG­MEDIUM. One of the nice things about doing things the IShell­Item­Array way is that the shell default implementation of IShell­Item­Array has all these defensive measures built-in so you don't have to write them yourself.

    Anyway, once we get the CIDA, we bind to the folder portion, walk through the items, and get the size of each item in order to print it. Same story, different words.

    Exercise: Why did we need a separate defensive measure for data objects which returned success but left garbage in the STG­MEDIUM? Why doesn't the Global­Lock test cover that case, too?

Page 3 of 3 (27 items) 123