2012-12-11

Windows Shell Extensions


Lâu lắm mới viết lại shell extension ...
Để viết Shell extensions thì có nhiều tutorials ko cần phải nói nữa, search 1 vòng trên CodeProject có thể list ra như series http://www.codeproject.com/Articles/441/The-Complete-Idiot-s-Guide-to-Writing-Shell-Extens hoặc các ví dụ trên CodePlex http://1code.codeplex.com/wikipage?title=WinShell

Hầu hết các ví dụ này dùng ATL, tất nhiên vậy cũng OK. Nhưng có 1 cách tiếp cận hay khá linh động viết COM bình thường không dùng ATL cần note lại là của TortoiseSVN. Đơn giản có thể tóm tắt như sau: ví dụ cần wrap 1 số shell extensions vào 1 chỉ DLL hoặc sử dụng trong trường hợp cần nhiều icon overlays mỗi cho 1 trạng thái của file như syncing, synced, do handler chỉ xử lý bằng IsMemberOf có nghĩa là chỉ xác định có/không hiển thị icon nên phải có 2 CLSID cho 2 trạng thái file.

Giả sử cần 2 icon overlay handlers cho syncing, synced và 1 context menu. Class xử lý icon overlay là IconOverlayExt và class xử lý context menu là ContextMenuExt. IconOverlayExt xử lý hiển thị hay không 2 trạng thái syncing và synced. Ta có các CLSID tương ứng CLSID_Synced, CLSID_Syncing, CLSID_ContextMenu.

Khai báo một enum xác định extension cần tạo:


enum ExtensionState
{
       Invalid,
       IconSynced,
       IconSyncing,
       ContextMenu
};


ShellExtClassFactory chịu trách nhiệm tại ext tùy theo loại ext cần tạo, nhận 1 ExtensionState là member


// This class factory object creates the main handlers -
// its constructor says which OLE class it has to make.
class ShellExtClassFactory : public IClassFactory
{
protected:
    ULONG _refCount;
    // Variable to contain class of object (i.e. syncing, synced, context menu)
    ExtensionState _stateToMake;
      
public:
    ShellExtClassFactory(ExtensionState stateToMake);
    virtual ~ShellExtClassFactory();
      
       // IUnknown members
    STDMETHODIMP         QueryInterface(REFIID, LPVOID FAR *);
    STDMETHODIMP_(ULONG) AddRef();
    STDMETHODIMP_(ULONG) Release();
   
       // IClassFactory members
    STDMETHODIMP           CreateInstance(LPUNKNOWN, REFIID, LPVOID FAR *);
    STDMETHODIMP           LockServer(BOOL);
};


Như vậy code DllGetClassObject như sau:


STDAPI DllGetClassObject(const CLSID& rclsid, const IID& riid, void** ppv)
{
       if(ppv == 0)
       {
              return E_POINTER;
       }

       *ppv = NULL;

       // State to get factory
       ExtensionState stateToMake = ExtensionState::Invalid;
       if (IsEqualIID(rclsid, CLSID_Synced))
       {
              stateToMake = ExtensionState::IconSynced;
       }
       else if (IsEqualIID(rclsid, CLSID_Syncing))
       {
              stateToMake = ExtensionState::IconSyncing;
       }
       else if (IsEqualIID(rclsid, CLSID_ContextMenu))
       {
              stateToMake = ExtensionState::ContextMenu;
       }

       if (stateToMake != ExtensionState::Invalid)
       {
              ShellExtClassFactory *classFactory = new (std::nothrow)ShellExtClassFactory(stateToMake);
              if (classFactory == NULL)
              {
                     return E_OUTOFMEMORY;
              }

              // IMPORTANT NOTE: ref count set to 0 at this moment -> do not call Release
              // If your ClassFactory object's reference count initialize with 0, DO NOT call the release method,
              // it seems the COM library would call the release method automatically.
              // Otherwise, you'd get an access exception.
              //
              // If ClassFactory initialize with ref count = 1 -> must call Release
              // See http://msdn.microsoft.com/en-us/library/windows/desktop/ms680760(v=vs.85).aspx
              const HRESULT hr = classFactory->QueryInterface(riid, ppv);
              if (FAILED(hr))
              {
                     delete classFactory;
              }            

              return hr;
       }

       return CLASS_E_CLASSNOTAVAILABLE;
}

NOTE: ở đây cũng lưu ý code COM trong TortoiseSVN khác với code mẫu ClassFactory hay COM hay thấy như trong MSDN chẳng hạn, sau khi QueryInterface sử dụng xong thường gọi Release tuy nhiên reference count TortoiseSVN init là zero trong khi ví dụ thông thường init là 1. Xem thêm http://msdn.microsoft.com/en-us/library/windows/desktop/ms680760(v=vs.85).aspx

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void **ppv)
{
    HRESULT hr = CLASS_E_CLASSNOTAVAILABLE;

    if (IsEqualCLSID(CLSID_FileContextMenuExt, rclsid))
    {
        hr = E_OUTOFMEMORY;

        ClassFactory *pClassFactory = new ClassFactory();
        if (pClassFactory)
        {
            hr = pClassFactory->QueryInterface(riid, ppv);
            pClassFactory->Release();
        }
    }

    return hr;
}



Như vậy là OK đã có ClassFactory và ext cần tạo, trong CreateInstance


STDMETHODIMP ShellExtClassFactory::CreateInstance(LPUNKNOWN unkOuter,
       REFIID riid, LPVOID *out)
{
       if(out == 0)
       {
              return E_POINTER;
       }

       *out = NULL;

       // Shell extensions typically don't support aggregation (inheritance)     
       if (unkOuter)
       {
              return CLASS_E_NOAGGREGATION;
       }

       HRESULT hr;  
       if(_stateToMake == ExtensionState::ContextMenu)
       {
              // The shell will then call QueryInterface with IID_IShellExtInit--this
              // is how shell extensions are initialized.
              ContextMenuExt* contextMenu = new (std::nothrow)ContextMenuExt();  // create the ContextMenuExt object
              if (contextMenu == nullptr)
              {
                     return E_OUTOFMEMORY;
              }

              hr = contextMenu->QueryInterface(riid, out);
              if(FAILED(hr))
              {
                     delete contextMenu;
              }
       }
       else
       {
              // Create the main shell extension object.
              IconOverlayExt* iconOverlay = new (std::nothrow)IconOverlayExt(_stateToMake);  // create the IconOverlayExt object
              if (iconOverlay == nullptr)
              {
                     return E_OUTOFMEMORY;
              }

              hr = iconOverlay->QueryInterface(riid, out);
              if(FAILED(hr))
              {
                     delete iconOverlay;
              }
       }

       return hr;
}

Thêm std::nothrow để kiểm tra kết quả trả về thay vì nhận exception. Tại đây dựa vào ext cần tạo sẽ xử lý.

Còn 1 việc cần giải quyết là IPC giữa shell và app. Có thể dùng RPC, DLL shared segment cũng là 1 khả năng tuy nhiên phải cùng x86 hay 64-bit (app x86 chẳng hạn không thể load shell ext 64-bit), và named pipe có lẽ là hợp lý nhất.

Debug

Về debug chỉ có 1 lưu ý, nếu trong máy có install nhiều app có ext thì nên disable hay tạm thời uninstall. Giả sử máy bạn là developer hay cài TortoiseSVN thì tạm thời bỏ nó đi vì không thể nào debug được, lý do TortoiseSVN sẽ block ngay explorer khi reach break point đâu đó, khi đó chỉ có nước nhấn reset máy (cry).

Để tránh các app khác load ext: 1 số thằng rất hay load như Google Chrome, IDM và Skype trong DLL Attach code 1 đoạn như sau nếu có debugger hay là app test-shell của mình hoặc Explorer hay regsvr32 thì OK:

BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
{
#ifdef _DEBUG
       // If no debugger is present, then don't load the dll.
       // this prevents other apps from loading the dll and locking
       // it.

       bool isInShellTest = false;
       TCHAR fullPath[MAX_PATH + 1];       // MAX_PATH ok, the test really is for debugging anyway.
       DWORD len = GetModuleFileName(NULL, fullPath, MAX_PATH);
       TCHAR fileNameWithoutExt[MAX_PATH + 1];
       TCHAR ext[MAX_PATH + 1];
      
       _tsplitpath_s(fullPath, NULL, 0, NULL, 0, fileNameWithoutExt, MAX_PATH, ext, MAX_PATH);

       TCHAR fileName[MAX_PATH + 1];
       _tmakepath_s(fileName, MAX_PATH, NULL, NULL, fileNameWithoutExt, ext);

       if ((_tcsicmp(fileName, _T("test-shell.exe"))) == 0 || (_tcsicmp(fileName, _T("explorer.exe"))) == 0 ||
              (_tcsicmp(fileName, _T("verclsid.exe"))) == 0 || (_tcsicmp(fileName, _T("regsvr32.exe"))) == 0)
       {
              isInShellTest = true;
       }

       if (!::IsDebuggerPresent() && !isInShellTest)
       {
              return FALSE;
       }
#endif

       switch (reason)
       {
              ...
       }

       return TRUE;
}


Để có thể debug thực hiện:
Thêm Properties -> Build Events -> Post-Build Event:
Command Line: echo "Registering Icon Overlay Library" regsvr32 "$(TargetPath)"

Thêm Debugging
Command: explorer.exe

Để restart Explorer unload ext DLL không tắt bằng Taskbar Manager mà thực hiện
Ctrl + Shift + Right Click vào khoảng trống như hình bên dưới
















No comments:

Post a Comment