• The Old New Thing

    Mr. Lee CatCam lets you see what a cat does all day

    • 4 Comments

    Jürgen Perthold modified a lightweight camera and hung it around his cat's neck, snapping a picture every few minutes. Join Mr. Lee on a trip through the neighborhood. If those trips aren't enough, you can also see what Jacquie is up to.

  • The Old New Thing

    There's always the low-tech way of managing a process, too

    • 15 Comments

    One of my colleagues had a problem with content management. I've changed the underlying scenario but the principle is the same.

    Is there a way to require that someone other than the author of a proposal sign off before the proposal tracking system accepts it? We had an issue where somebody wrote up a proposal, and due to a miscommunication, the proposal coordinator thought the proposal was ready and submitted it prematurely. This happened to another team in our group, and we want to make sure we don't make the same mistake.

    Another colleague explained:

    This is a people problem, not a technology problem. One way to work around it is to tell the proposal coordinator, "Don't submit the proposal until I sent you email that says it's okay to submit the proposal."

    I agree that this was a people problem, but the offered solution suffers from the same miscommunication problem as the original. The proposal coordinator might ask, "Is the proposal ready?" and the author responds, "It'll be ready tomorrow." The next morning, the proposal coordinator submits the proposal assuming that the author's response meant "Submit it tomorrow," when the author actually meant "You will get an email message from me tomorrow when it's ready."

    My colleague responded that the technique still has a single point of failure: An error by one person (the proposal coordinator or the proposal author—you decide who is at fault) results in the proposal to be submitted prematurely. We want to make sure two people sign off on the proposal before it is submitted.

    I proposed a method popularized by Henry Ford: The assembly line.

    • Author writes proposal. Places proposal in Location 1.
    • Proposal is reviewed by reviewer A. When it passes review, it is moved to Location 2.
    • Proposal is reviewed by reviewer B. When it passes review, it is moved to Location 3.
    • Proposal coordinator picks up proposals from Location 3 and submits them.

    With this scheme, every proposal must be approved by both reviewer A and reviewer B. If reviewer A fails to approve the proposal, then it remains in location 1. If reviewer B fails to approve the proposal, then it remains in location 2.

    This is another one of those simple low-tech solutions: Instead of putting all proposals (complete and incomplete) in one location, the location of the proposal represents its approval state.

    Of course, you can add more bells and whistles to this technique. For example, you can allow reviews in parallel by having Location 1 mean "unapproved", Location 2a mean "approved by reviewer A only", Location 2b mean "approved by reviewer B only", and Location 3 mean "approved by both reviewers." I'm sure you can come up with other tweaks. (I'm assuming that the proposal file format doesn't support custom fields like "signed off by".)

  • The Old New Thing

    Debug session: Why is an LPC server not responding?

    • 16 Comments

    A particular scenario was hanging, and the team responsible for the scenario debugged it to the point where they saw that their X component was waiting for their Y component, which was waiting for Explorer, so they asked for help chasing the hang into Explorer.

    The team was kind enough to have shared what they've learned so far:

    kd> !alpc /m 9c14d020
    
    Message 9c14d020
      MessageID             : 0x0274 (628)
      CallbackID            : 0xCCA5 (52389)
      SequenceNumber        : 0x00000016 (22)
      Type                  : LPC_REQUEST
      DataLength            : 0x0094 (148)
      TotalLength           : 0x00AC (172)
      Canceled              : No
      Release               : No
      ReplyWaitReply        : No
      Continuation          : Yes
      OwnerPort             : 82bb9db8 [ALPC_CLIENT_COMMUNICATION_PORT]
      WaitingThread         : 834553c0
      QueueType             : ALPC_MSGQUEUE_MAIN
      QueuePort             : 84646730 [ALPC_CONNECTION_PORT]
      QueuePortOwnerProcess : 846209c0 (explorer.exe)
      ServerThread          : 00000000 <-----------------------
      QuotaCharged          : No
      CancelQueuePort       : 00000000
      CancelSequencePort    : 00000000
      CancelSequenceNumber  : 0x00000000 (0)
      ClientContext         : 02a56b80
      ServerContext         : 00000000
      PortContext           : 0701ea20
      CancelPortContext     : 00000000
      SecurityData          : 962f89b8
      View                  : 00000000
    kd> !process 846209c0 0
    PROCESS 846209c0  SessionId: 1  Cid: 0804    Peb: 7fbac000  ParentCid: 0724
        DirBase: 3e546380  ObjectTable: 97195300  HandleCount: 1041.
        Image: explorer.exe
    

    Yikes, there is no thread signed up to service the request.

    I don't know much about ALPC, but I can fumble around. Fortunately, this is debugging and not rocket surgery, so you still get full points if you stumble across the answer by guessing.

    I decided to start guessing by looking at what the !alpc command can tell me.

    kd> !alpc -?
    
      !alpc /m MessageAddress
        Dumps the message at the specified address.
    
      !alpc /p PortAddress
        Dumps the port at the specified address.
    

    Well, I already saw what the result was for dumping the message, so I may as well dump the port.

    kd> !alpc /p 84646730
    
    ...
      8 thread(s) are registered with port IO completion object:
        THREAD 84658d40  Cid 0804.0888  Teb: 7fa7e000 Win32Thread: 8214a748 WAIT
        THREAD 8466a040  Cid 0804.08c4  Teb: 7fa74000 Win32Thread: 8214c800 WAIT
        THREAD 84659a00  Cid 0804.08ec  Teb: 7fa72000 Win32Thread: 82158d08 WAIT
        THREAD 8466c8c0  Cid 0804.08f0  Teb: 7fa6e000 Win32Thread: 82160420 WAIT
        THREAD 84671040  Cid 0804.0910  Teb: 7fa68000 Win32Thread: 8217c4e8 WAIT
        THREAD 8460d180  Cid 0804.099c  Teb: 7fa5e000 Win32Thread: 820bad08 WAIT
        THREAD 834278c0  Cid 0804.0c80  Teb: 7fa6b000 Win32Thread: 820b9620 WAIT
        THREAD 8345ad40  Cid 0804.0da0  Teb: 7fba9000 Win32Thread: 821c6d08 WAIT
    ...
    

    So it looks like there are eight threads signed up to process events on this port. (Is that what this means? I don't know, but I'm going to assume that it does, because this is debugging. Debugging is an exercise in optimism.) Let's see what they're doing.

    kd> .thread 84658d40;k
    Implicit thread is now 84658d40
      *** Stack trace for last set context - .thread/.cxr resets it
    ChildEBP RetAddr
    940ef394 80f1505f nt!KiSwapContext+0x19
    940ef3d0 80f184e0 nt!KiSwapThread+0x34b
    940ef3fc 80f163fc nt!KiCommitThreadWait+0x26f
    940ef46c 80f4d2df nt!KeWaitForSingleObject+0x459
    940ef4b4 80e20838 nt!KiSchedulerApc+0x298
    940ef4c8 00000000 hal!KfLowerIrql+0x2c
    
    [the others look the same]
    

    Well, I don't know what they're doing, but it looks like they're waiting for something. But one of the threads looks different:

    kd> .thread 84671040;k
    Implicit thread is now 84671040
      *** Stack trace for last set context - .thread/.cxr resets it
    ChildEBP RetAddr
    9415f864 80f1505f nt!KiSwapContext+0x19
    9415f8a0 80f184e0 nt!KiSwapThread+0x34b
    9415f8cc 80eb3d6e nt!KiCommitThreadWait+0x26f
    9415f934 810c0527 nt!KeWaitForMultipleObjects+0x4e3
    9415fbe4 810c0703 nt!ObWaitForMultipleObjects+0x2fd
    9415fd38 80ef113c nt!NtWaitForMultipleObjects+0xca
    9415fd38 77945e04 nt!KiFastCallEntry+0x12c
    07e2f1c4 779437f6 ntdll!KiFastSystemCallRet
    07e2f1c8 7515c136 ntdll!NtWaitForMultipleObjects+0xa
    07e2f34c 77752658 KERNELBASE!WaitForMultipleObjectsEx+0xee
    07e2f368 777fbe60 KERNEL32!WaitForMultipleObjects+0x19
    07e2f3d4 777fc5de KERNEL32!WerpReportFaultInternal+0x1a3
    07e2f3e8 777df654 KERNEL32!WerpReportFault+0x6d
    07e2f3f4 751e517c KERNEL32!BasepReportFault+0x19
    07e2f490 77a0f95a KERNELBASE!UnhandledExceptionFilter+0x1e0
    07e2f4a0 77a0fd4d ntdll!TppExceptionFilter+0x1b
    07e2f4b4 77a1c66b ntdll!TppWorkerpInnerExceptionFilter+0x13
    07e2fb34 77753278 ntdll!TppWorkerThread+0xa6092
    07e2fb40 779761a6 KERNEL32!BaseThreadInitThunk+0xe
    07e2fb80 77976152 ntdll!__RtlUserThreadStart+0x4a
    07e2fb90 00000000 ntdll!_RtlUserThreadStart+0x1c
    

    Ah, well that explains why Explorer isn't responding: It crashed on an unhandled exception! Windows Error Reporting is busy trying to generate a report.

    Now to see what the crash was. I don't know for sure, but I'm pretty confident that one of the parameters to Basep­Report­Fault is an EXCEPTION_POINTERS. Why am I confident of that? Because it would be hard to report the fault without it!

    kd> dd 07e2f3f4 l4
    07e2f3f4  07e2f490 77a0f95a 07e2f4e8 00000001
              ChildEBP RetAddr  Param1
    
    kd> dd 07e2f4e8 l2
    07e2f4e8  07e2f620 07e2f63c 
                     ^ ^
       ExceptionRecord ContextRecord
    kd> .cxr 0x07e2f63c
    eax=00000000 ebx=0451e2f8 ecx=e2af034f edx=77945e00 esi=00000000 edi=0451e2e0
    eip=1df7fc6a esp=07e2f920 ebp=07e2f938 iopl=0         nv up ei pl zr na pe nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
    contoso!ContosoPower::Disconnect+0xdd:
    001b:1df7fc6a 8b08  mov ecx,dword ptr [eax] ds:0023:00000000=????????
    

    Aha, Explorer crashed due to a null pointer crash in the Contoso­Power::Disconnect function.

    Passing the buck onward to Contoso, the report back was that this was a known issue, and a hotfix was available.

  • The Old New Thing

    Why is there a 64KB no-man's land near the end of the user-mode address space?

    • 29 Comments

    We learned some time ago that there is a 64KB no-man's land near the 2GB boundary to accommodate a quirk of the Alpha AXP processor architecture. But that's not the only reason why it's there.

    The no-man's land near the 2GB boundary is useful even on x86 processors because it simplifies parameter validation at the boundary between user mode and kernel mode by taking out a special case. If the 64KB zone did not exist, then somebody could pass a buffer that straddles the 2GB boundary, and the kernel mode validation layer would have to detect that unusual condition and reject the buffer.

    By having a guaranteed invalid region, the kernel mode buffer validation code can simply validate that the starting address is below the 2GB boundary, then walk through the buffer checking each page. If somebody tries to straddle the boundary, the validation code will hit the permanently-invalid region and fail.

    Yes, this sounds like a micro-optimization, but I suspect this was not so much for optimization purposes as it was to remove weird boundary conditions, because weird boundary conditions are where the bugs tend to be.

    (Obviously, the no-man's land moves if you set the /3GB switch.)

  • The Old New Thing

    News flash: Companies change their product to appeal to their customers

    • 24 Comments

    There was some apparent uproar because there was an industry which "changed the flavoring of their product depending on which market segment they were trying to appeal to."

    Well duh, don't all industries do this?

    The reason why this even remotely qualified as news didn't appear until the last five words of the article!

  • The Old New Thing

    Menu item states are not reliable until they are shown because they aren't needed until then

    • 10 Comments

    A question arrived from a customer (with the rather unhelpful subject line Question for Microsoft) wondering why, when they call Get­System­Menu and then ask for the states of the various menu items like SC_MINIMIZE, the menu item states don't reflect reality. The menu item states don't synchronize with reality until the user actually opens the system menu.

    There is no requirement that applications keep menu item states continuously in sync. After all, that's why we have messages like WM_INIT­MENU: To tell the application, "Whoa, we're about to show this menu, so you might want to comb its hair and pick the food out of its teeth so it can be seen by the user." Lazy evaluation is common, because maintaining states continuously can be expensive, and there's no point constantly turning items on and of and on and off if the user can't see them anyway.

    This is double-true for system menus, because maintaining the states continuously is not possible when the system menu is being shared across windows. The menu states are not synchronized to the window until the menu is about to be displayed.

    If you want to know whether the SC_MINIMIZE menu item would be enabled if the menu were shown, you can check the window styles: A window can be minimized if it has a WS_MINIMIZE­BOX and is not already WS_MINIMIZEd. Similar logic can be applied to the other menu items.

    Well, except for SC_CLOSE. While in most cases the window caption determines what is enabled on the menu, the Close button works backward: It is the state of the menu item that controls whether the Close button is enabled. So in the special case of SC_CLOSE, you can query the state at any time, because for that case, the menu controls the state rather than simply reflecting it.

    Why is SC_CLOSE so special? Here come da history.

    The Close button was added in Windows 95. Since versions of Windows prior to Windows 95 didn't have a Close button, they didn't need a style to specify whether the Close button should be enabled or not. (You don't need a style to control something that doesn't exist.) Windows 95 added the Close button and hooked it up to the only thing that it had available, namely, the SC_CLOSE item on the system menu. Sure, Windows 95 could have have invented a new window style, but since SC_CLOSE already existed and applications were already using it, using SC_CLOSE to control the Close button allowed old applications to reap the benefits of the new Close button automatically. It also meant that there was one less thing you had to change when porting your program to Windows 95.

    Bonus chatter: You can now answer Alex Cohn's question:

    I wonder if the EnableMenuItem method will work for minimize and maximize, too. After all, these buttons also have siblings in the Alt-space menu.
  • The Old New Thing

    How can I get the URL to the Web page the clipboard was copied from?

    • 18 Comments

    When you copy content from a Web page to the clipboard and then paste it into OneNote, OneNote pastes the content but also annotates it "Pasted from ...". How does OneNote know where the content was copied from?

    As noted in the documentation for the HTML clipboard format, Web browsers can provide an optional Source­URL property to specify the Web page the HTML was copied from.

    Let's write a Little Program that mimics what OneNote does, but just in plain text, because I don't want to try to parse HTML. This is much easier to do in C#, because the BCL provides most of the helper functions.

    using System;
    using System.IO;
    using System.Windows;
    
    class Program {
     [STAThread]
     public static void Main() {
      System.Console.WriteLine(Clipboard.GetText());
      using (var sr = new StringReader(
                   Clipboard.GetText(TextDataFormat.Html))) {
       string s;
       while ((s = sr.ReadLine()) != null) {
        if (s.StartsWith("SourceURL:")) {
         System.Console.WriteLine("Copied from {0}", s.Substring(10));
         break;
        }
       }
      }
     }
    }
    

    First, we get the text from the clipboard and print it. That's the easy part.

    Next, we get the HTML text from the clipboard. This is a bunch of text in a particular format. We look for an entry that specifies the Source­URL; if we find it, then we print the URL.

    This code is rather sloppy. For example, if the HTML itself contains the string SourceURL:haha-fakeout, we risk misdetecting it as the source. To do this properly, we would have to verify that the string appears in the header area of the HTML (before the first StartFragment).

    But this is a Little Program, so I can skip all that stuff.

    Here's a sketch of the equivalent C/C++ version:

    int __cdecl main(int, char **)
    {
     if (OpenClipboard(NULL)) {
    
      // Obtain the Unicode text and print it
      HANDLE h = GetClipboardData(CF_UNICODETEXT);
      if (h) {
       PCWSTR pszPlainText = GlobalLock(h);
       ... print pszPlainText ...
       GlobalUnlock(h);
      }
    
      // Obtain the HTML text and extract the SourceURL
      h = GetClipboardData(RegisterClipboardFormat(TEXT("HTML Format")));
      if (h) {
       PCSTR pszHtmlFormat = GlobalLock(h);
       ... break pszHtmlFormat into lines ...
       ... look for a line that begins with "SourceURL:" ...
       ... if found, print it ...
       GlobalUnlock(h);
      }
      CloseClipboard();
     }
     return 0;
    }
    
  • The Old New Thing

    Your program assumes that COM output pointers are initialized on failure; you just don't realize it yet

    • 16 Comments

    We saw last time that the COM rules for output pointers are that they must be initialized on return from a function, even if the function fails. The COM marshaller relies on this behavior, but then again, so do you; you just don't realize it yet.

    If you use a smart pointer library (be it ATL or boost or whatever), you are still relying on output pointers being NULL when not valid, regardless of whether or not the call succeeded. Let's look at this line of code from that article about IUnknown::QueryInterface:

    CComQIPtr<ISomeInterface> spsi(punkObj);
    ...
    // spsi object goes out of scope
    

    If the IUnknown::QueryInterface method puts a non-NULL value in spsi on failure, then when spsi is destructed, it's going to call IUnknown::Release on itself, and something bad happens. If you're lucky, you will crash because the thing lying around in spsi was a garbage pointer. But if you're not lucky, the thing lying around in spsi might be a pointer to a COM object:

    // wrong!
    HRESULT CObject::QueryInterface(REFIID riid, void **ppvObj)
    {
      *ppvObj = this; // assume success since it almost always succeeds
      if (riid == IID_IUnknown || riid == IID_IOtherInterface) {
        AddRef();
        return S_OK;
      }
      // forgot to set *ppvObj = NULL
      return E_NOINTERFACE;
    }
    

    Notice that this code optimistically sets the output pointer to itself, but if the interface is not supported, it changes its mind and returns E_NOINTERFACE without setting the output pointer to NULL. Now you have an elusive reference counting bug, because the destruction of spsi will call CObject::Release, which will manifest itself by CObject object being destroyed prematurely because you just over-released the object. If you're lucky, that'll happen relative soon; if you're not lucky, it won't manifest itself for another half hour.

    Okay, sure, maybe this is too obvious a mistake for CObject::QueryInterface, but any method that has an output parameter can suffer from this error, and in those cases it might not be quite so obvious:

    // wrong!
    HRESULT CreateSurface(const SURFACEDESC *psd,
                          ISurface **ppsf)
    {
     *ppsf = new(nothrow) CSurface();
     if (!*ppsf) return E_OUTOFMEMORY;
     HRESULT hr = (*ppsf)->Initialize(psd);
     if (SUCCEEDED(hr)) return S_OK;
     (*ppsf)->Release(); // throw it away
     // forgot to set *ppsf = NULL
     return hr;
    }
    

    This imaginary function takes a surface description and tries to create a surface that matches it. It does this by first creating a blank surface, and then initializing the surface. If that succeeds, then we succeed; otherwise, we clean up the incomplete surface and fail.

    Except that we forgot to set *ppsf = NULL in our failure path. If initialization fails, then we destroy the surface, and the pointer returned to the caller points to the surface that we abandoned. But the caller shouldn't be looking at that pointer because the function failed, right?

    Well, unless the caller called you like this:

    CComPtr<ISurface> spsf;
    if (SUCCEEDED(CreateSurface(psd, &spsf))) {
     ...
    }
    

    If the surface fails to initialize, then spsf contains a pointer to a surface that has already been deleted. When the spsf is destructed, it's going to call ISurface::Release on some point that is no longer valid, and bad things are going to happen. This can get particularly insidious when spsf is not a simple local variable but rather a member of class which itself doesn't get destroyed for a long time. The bad pointer sits in m_spsf like a time bomb.

    Although all the examples I gave here involve COM interface pointers, the rule applies to all output parameters.

    CComBSTR bs;
    if (SUCCEEDED(GetName(&bs)) { ... }
    
    // -or-
    
    CComVariant var;
    if (SUCCEEDED(GetName(&var)) { ... }
    

    In the first case, the the GetName method had better not leave garbage in the output BSTR on failure, because the CComBSTR is going to SysFreeString in its destructor. Similarly in the second case with CComVariant and VariantClear.

    So remember, if your function doesn't want to return a value in an output pointer, you still have to return something in it.

  • The Old New Thing

    Invoking commands on items in the Recycle Bin

    • 14 Comments

    Once you've found the items you want in the Recycle Bin, you may want to perform some operation on them. This brings us back to our old friend, IContextMenu. At this point, you're just snapping two blocks together. You have one block called Retrieving properties from items in the Recycle Bin and you have another block called Invoking verbs on items.

    For the first block, let's assume you've written a function called WantToRestoreThisItem which studies the properties of a Recycle Bin item and determines whether you want to restore it. I leave this for you to implement, since I don't know what your criteria are. Maybe you want to restore files only if they were deleted from a particular directory. Maybe you want to restore files that were deleted while you were drunk. (This assumes you have some other computer program that tracks when you're drunk.)¹ Whatever. It's your function.

    For the second block, we have a helper function which should look awfully familiar.

    void InvokeVerb(IContextMenu *pcm, PCSTR pszVerb)
    {
     HMENU hmenu = CreatePopupMenu();
     if (hmenu) {
      HRESULT hr = pcm->QueryContextMenu(hmenu, 0, 1, 0x7FFF, CMF_NORMAL);
      if(SUCCEEDED(hr)) {
       CMINVOKECOMMANDINFO info = { 0 };
       info.cbSize = sizeof(info);
       info.lpVerb = pszVerb;
       pcm->InvokeCommand(&info);
      }
      DestroyMenu(hmenu);
     }
    }
    

    And now we snap the two blocks together.

    int __cdecl _tmain(int argc, PTSTR *argv)
    {
     HRESULT hr = CoInitialize(NULL);
     if (SUCCEEDED(hr)) {
      IShellItem *psiRecycleBin;
      hr = SHGetKnownFolderItem(FOLDERID_RecycleBinFolder, KF_FLAG_DEFAULT,
                                NULL, IID_PPV_ARGS(&psiRecycleBin));
      if (SUCCEEDED(hr)) {
       IEnumShellItems *pesi;
       hr = psiRecycleBin->BindToHandler(NULL, BHID_EnumItems,
                                         IID_PPV_ARGS(&pesi));
       if (hr == S_OK) {
        IShellItem *psi;
        while (pesi->Next(1, &psi, NULL) == S_OK) {
         if (WantToRestoreThisItem(psi)) {
          IContextMenu *pcm;
          hr = psi->BindToHandler(NULL, BHID_SFUIObject,
                                  IID_PPV_ARGS(&pcm));
          if (SUCCEEDED(hr)) {
           InvokeVerb(pcm, "undelete");
           pcm->Release();
          }
         }
         psi->Release();
        }
       }
       psiRecycleBin->Release();
      }
      CoUninitialize();
     }
     return 0;
    }
    

    One annoyance of the Recycle Bin is that, at least up until Windows 7, it ignores the CMIC_MASK_FLAG_NO_UI flag. It always displays a confirmation dialog if something dangerous is about to happen (like overwriting an existing file). To mitigate this problem, we can at least reduce the number of confirmations from one-per-file to just one by batching up all the objects we want to operate on into a single context menu. For this, it's easier to go back to the classical version of the program.

    int __cdecl _tmain(int argc, PTSTR *argv)
    {
     HRESULT hr = CoInitialize(NULL);
     if (SUCCEEDED(hr)) {
      IShellFolder2 *psfRecycleBin;
      hr = BindToCsidl(CSIDL_BITBUCKET, IID_PPV_ARGS(&psfRecycleBin));
      if (SUCCEEDED(hr)) {
       IEnumIDList *peidl;
       hr = psfRecycleBin->EnumObjects(NULL,
         SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &peidl);
       if (hr == S_OK) {
        // in a real program you wouldn't hard-code a fixed limit
        PITEMID_CHILD rgpidlItems[100];
        UINT cpidlItems = 0;
        PITEMID_CHILD pidlItem;
        while (peidl->Next(1, &pidlItem, NULL) == S_OK) {
         if (WantToRestoreThisItem(psfRecycleBin, pidlItem) &&
             cpidlItems < ARRAYSIZE(rgpidlItems)) {
          rgpidlItems[cpidlItems++] = pidlItem;
         } else {
          CoTaskMemFree(pidlItem);
         }
        }
        // restore the items we collected
        if (cpidlItems) {
         IContextMenu *pcm;
         hr = psfRecycleBin->GetUIObjectOf(NULL, cpidlItems,
                         (PCUITEMID_CHILD_ARRAY)rgpidlItems,
                         IID_IContextMenu, NULL, (void**)&pcm);
         if (SUCCEEDED(hr)) {
          InvokeVerb(pcm, "undelete");
          pcm->Release();
         }
         for (UINT i = 0; i < cpidlItems; i++) {
          CoTaskMemFree(rgpidlItems[i]);
         }
        }
       }
       psfRecycleBin->Release();
      }
      CoUninitialize();
     }
     return 0;
    }
    

    In the course of the enumeration, we save the ITEMIDLISTs of all the items we want to restore, then create one giant context menu for all of them. This is the programmatic equivalent of multi-selecting the items from the Recycle Bin and then right-clicking. We then invoke the undelete verb on the entire group.

    Okay, so now suppose you want to restore the files, but instead of restoring them to their original locations, you want to restore them to a special folder. Like, say, C:\Files I deleted while I was drunk.¹ No problem. We just need a different block to snap into: The drag/drop block.

    void DropOnRestoreFolder(IDataObject *pdto)
    {
     IDropTarget *pdt;
     if (SUCCEEDED(GetUIObjectOfFile(NULL,
            L"C:\\Files I deleted while I was drunk",
            IID_PPV_ARGS(&pdt)))) {
      POINTL pt = { 0, 0 };
      DWORD dwEffect = DROPEFFECT_MOVE;
      if (SUCCEEDED(pdt->DragEnter(pdto, MK_LBUTTON,
                                   pt, &dwEffect))) {
       dwEffect &= DROPEFFECT_MOVE;
       if (dwEffect) {
        pdt->Drop(pdto, MK_LBUTTON, pt, &dwEffect);
       } else {
        pdt->DragLeave();
       }
      }
      pdt->Release();
     }
    }
    

    And now it's just a matter of snapping out the undelete block and snapping in the drag/drop block.

    int __cdecl _tmain(int argc, PTSTR *argv)
    {
     HRESULT hr = CoInitialize(NULL);
     if (SUCCEEDED(hr)) {
      IShellFolder2 *psfRecycleBin;
      hr = BindToCsidl(CSIDL_BITBUCKET, IID_PPV_ARGS(&psfRecycleBin));
      if (SUCCEEDED(hr)) {
       IEnumIDList *peidl;
       hr = psfRecycleBin->EnumObjects(NULL,
         SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &peidl);
       if (hr == S_OK) {
        // in a real program you wouldn't hard-code a fixed limit
        PITEMID_CHILD rgpidlItems[100];
        UINT cpidlItems = 0;
        PITEMID_CHILD pidlItem;
        while (peidl->Next(1, &pidlItem, NULL) == S_OK) {
         if (WantToRestoreThisItem(psfRecycleBin, pidlItem) &&
             cpidlItems < ARRAYSIZE(rgpidlItems)) {
          rgpidlItems[cpidlItems++] = pidlItem;
         } else {
          CoTaskMemFree(pidlItem);
         }
        }
        // restore the items we collected
        if (cpidlItems) {
         IDataObject *pdto;
         hr = psfRecycleBin->GetUIObjectOf(NULL, cpidlItems,
                         (PCUITEMID_CHILD_ARRAY)rgpidlItems,
                         IID_IDataObject, NULL, (void**)&pdto);
         if (SUCCEEDED(hr)) {
          DropOnRestoreFolder(pdto);
          pdto->Release();
         }
         for (UINT i = 0; i < cpidlItems; i++) {
          CoTaskMemFree(rgpidlItems[i]);
         }
        }
       }
       psfRecycleBin->Release();
      }
      CoUninitialize();
     }
     return 0;
    }
    

    Footnotes

    ¹ If being drunk isn't your thing, then substitute some other form of impaired judgment.

  • The Old New Thing

    Working with ambiguous and invalid points in time in managed code

    • 16 Comments

    Public Service Announcement: Daylight Saving Time ends in most parts of the United States this weekend.

    I pointed out some time ago that Win32 and .NET deal with daylight saving time differently. Specifically, Win32 always deals with the time zone you are currently in (even if it's not the time zone that corresponds to the timestamp you are manipulating), whereas .NET deals with the time zone that was in effect at the time the timestamp was generated.

    For more details on the latter, I refer you to Josh Free from the BCL Team Blog, who some time ago explained how to work with ambiguous and invalid points in time in managed code.

Page 370 of 434 (4,340 items) «368369370371372»