[SHELL32] Notify filesystem changes (#2659)
authorKatayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com>
Mon, 4 May 2020 05:53:23 +0000 (14:53 +0900)
committerGitHub <noreply@github.com>
Mon, 4 May 2020 05:53:23 +0000 (14:53 +0900)
Notify filesystem change notifications by using ReadDirectoryChangesW function. Creating/Deleting files/folders will be responsive in Explorer. CORE-13950

dll/win32/shell32/CDefView.cpp
dll/win32/shell32/changenotify.cpp
dll/win32/shell32/shelldesktop/CChangeNotifyServer.cpp

index fbca533..06d7c65 100644 (file)
@@ -1184,8 +1184,11 @@ LRESULT CDefView::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandl
         ntreg[0].fRecursive = FALSE;
         ntreg[0].pidl = m_pidlParent;
     }
-    m_hNotify = SHChangeNotifyRegister(m_hWnd, SHCNRF_NewDelivery | SHCNRF_ShellLevel,
-                                       SHCNE_ALLEVENTS, SHV_CHANGE_NOTIFY, nRegCount, ntreg);
+    m_hNotify = SHChangeNotifyRegister(m_hWnd,
+                                       SHCNRF_InterruptLevel | SHCNRF_ShellLevel |
+                                       SHCNRF_NewDelivery,
+                                       SHCNE_ALLEVENTS, SHV_CHANGE_NOTIFY,
+                                       nRegCount, ntreg);
     if (nRegCount == 3)
     {
         ILFree(pidls[0]);
index 3f2b7cf..ae5e0e8 100644 (file)
@@ -573,7 +573,7 @@ SHChangeNotification_Lock(HANDLE hTicket, DWORD dwOwnerPID, LPITEMIDLIST **lppid
     if (lppidls)
         *lppidls = pHandbag->pidls;
     if (lpwEventId)
-        *lpwEventId = pHandbag->pTicket->wEventId;
+        *lpwEventId = (pHandbag->pTicket->wEventId & ~SHCNE_INTERRUPT);
 
     // return the handbag
     return pHandbag;
index 5c2184f..a585297 100644 (file)
  */
 #include "shelldesktop.h"
 #include "shlwapi_undoc.h"
-#include <atlsimpcoll.h>
-#include <assert.h>
+#include <atlsimpcoll.h> // for CSimpleArray
+#include <process.h>     // for _beginthreadex
+#include <assert.h>      // for assert
 
 WINE_DEFAULT_DEBUG_CHANNEL(shcn);
 
+static inline void
+NotifyFileSystemChange(LONG wEventId, LPCWSTR path1, LPCWSTR path2)
+{
+    SHChangeNotify(wEventId | SHCNE_INTERRUPT, SHCNF_PATHW, path1, path2);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// DIRLIST --- directory list
+
+struct DIRLISTITEM
+{
+    WCHAR szPath[MAX_PATH];
+    DWORD dwFileSize;
+    BOOL fDir;
+
+    DIRLISTITEM(LPCWSTR pszPath, DWORD dwSize, BOOL is_dir)
+    {
+        lstrcpynW(szPath, pszPath, _countof(szPath));
+        dwFileSize = dwSize;
+        fDir = is_dir;
+    }
+
+    BOOL IsEmpty() const
+    {
+        return szPath[0] == 0;
+    }
+
+    BOOL EqualPath(LPCWSTR pszPath) const
+    {
+        return lstrcmpiW(szPath, pszPath) == 0;
+    }
+};
+
+class DIRLIST
+{
+public:
+    DIRLIST()
+    {
+    }
+
+    DIRLIST(LPCWSTR pszDir, BOOL fRecursive)
+    {
+        GetDirList(pszDir, fRecursive);
+    }
+
+    BOOL AddItem(LPCWSTR pszPath, DWORD dwFileSize, BOOL fDir);
+    BOOL GetDirList(LPCWSTR pszDir, BOOL fRecursive);
+    BOOL Contains(LPCWSTR pszPath, BOOL fDir) const;
+    BOOL RenameItem(LPCWSTR pszPath1, LPCWSTR pszPath2, BOOL fDir);
+    BOOL DeleteItem(LPCWSTR pszPath, BOOL fDir);
+    BOOL GetFirstChange(LPWSTR pszPath) const;
+
+    void RemoveAll()
+    {
+        m_items.RemoveAll();
+    }
+
+protected:
+    CSimpleArray<DIRLISTITEM> m_items;
+};
+
+BOOL DIRLIST::Contains(LPCWSTR pszPath, BOOL fDir) const
+{
+    assert(!PathIsRelativeW(pszPath));
+
+    for (INT i = 0; i < m_items.GetSize(); ++i)
+    {
+        if (m_items[i].IsEmpty() || fDir != m_items[i].fDir)
+            continue;
+
+        if (m_items[i].EqualPath(pszPath))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+BOOL DIRLIST::AddItem(LPCWSTR pszPath, DWORD dwFileSize, BOOL fDir)
+{
+    assert(!PathIsRelativeW(pszPath));
+
+    if (dwFileSize == INVALID_FILE_SIZE)
+    {
+        WIN32_FIND_DATAW find;
+        HANDLE hFind = FindFirstFileW(pszPath, &find);
+        if (hFind == INVALID_HANDLE_VALUE)
+            return FALSE;
+        FindClose(hFind);
+        dwFileSize = find.nFileSizeLow;
+    }
+
+    DIRLISTITEM item(pszPath, dwFileSize, fDir);
+    return m_items.Add(item);
+}
+
+BOOL DIRLIST::RenameItem(LPCWSTR pszPath1, LPCWSTR pszPath2, BOOL fDir)
+{
+    assert(!PathIsRelativeW(pszPath1));
+    assert(!PathIsRelativeW(pszPath2));
+
+    for (INT i = 0; i < m_items.GetSize(); ++i)
+    {
+        if (m_items[i].fDir == fDir && m_items[i].EqualPath(pszPath1))
+        {
+            lstrcpynW(m_items[i].szPath, pszPath2, _countof(m_items[i].szPath));
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+BOOL DIRLIST::DeleteItem(LPCWSTR pszPath, BOOL fDir)
+{
+    assert(!PathIsRelativeW(pszPath));
+
+    for (INT i = 0; i < m_items.GetSize(); ++i)
+    {
+        if (m_items[i].fDir == fDir && m_items[i].EqualPath(pszPath))
+        {
+            m_items[i].szPath[0] = 0;
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
+BOOL DIRLIST::GetDirList(LPCWSTR pszDir, BOOL fRecursive)
+{
+    // get the full path
+    WCHAR szPath[MAX_PATH];
+    lstrcpynW(szPath, pszDir, _countof(szPath));
+    assert(!PathIsRelativeW(szPath));
+
+    // is it a directory?
+    if (!PathIsDirectoryW(szPath))
+        return FALSE;
+
+    // add the path
+    if (!AddItem(szPath, 0, TRUE))
+        return FALSE;
+
+    // enumerate the file items to remember
+    PathAppendW(szPath, L"*");
+    WIN32_FIND_DATAW find;
+    HANDLE hFind = FindFirstFileW(szPath, &find);
+    if (hFind == INVALID_HANDLE_VALUE)
+    {
+        ERR("FindFirstFileW failed\n");
+        return FALSE;
+    }
+
+    do
+    {
+        // ignore "." and ".."
+        if (lstrcmpW(find.cFileName, L".") == 0 ||
+            lstrcmpW(find.cFileName, L"..") == 0)
+        {
+            continue;
+        }
+
+        // build a path
+        PathRemoveFileSpecW(szPath);
+        if (lstrlenW(szPath) + lstrlenW(find.cFileName) + 1 > MAX_PATH)
+        {
+            ERR("szPath is too long\n");
+            continue;
+        }
+        PathAppendW(szPath, find.cFileName);
+
+        // add the path and do recurse
+        if (fRecursive && (find.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
+            GetDirList(szPath, fRecursive);
+        else
+            AddItem(szPath, find.nFileSizeLow, FALSE);
+    } while (FindNextFileW(hFind, &find));
+
+    FindClose(hFind);
+
+    return TRUE;
+}
+
+BOOL DIRLIST::GetFirstChange(LPWSTR pszPath) const
+{
+    // validate paths
+    for (INT i = 0; i < m_items.GetSize(); ++i)
+    {
+        if (m_items[i].IsEmpty())
+            continue;
+
+        if (m_items[i].fDir) // item is a directory
+        {
+            if (!PathIsDirectoryW(m_items[i].szPath))
+            {
+                // mismatched
+                lstrcpynW(pszPath, m_items[i].szPath, MAX_PATH);
+                return TRUE;
+            }
+        }
+        else // item is a normal file
+        {
+            if (!PathFileExistsW(m_items[i].szPath) ||
+                PathIsDirectoryW(m_items[i].szPath))
+            {
+                // mismatched
+                lstrcpynW(pszPath, m_items[i].szPath, MAX_PATH);
+                return TRUE;
+            }
+        }
+    }
+
+    // check sizes
+    HANDLE hFind;
+    WIN32_FIND_DATAW find;
+    for (INT i = 0; i < m_items.GetSize(); ++i)
+    {
+        if (m_items[i].IsEmpty() || m_items[i].fDir)
+            continue;
+
+        // get size
+        hFind = FindFirstFileW(m_items[i].szPath, &find);
+        FindClose(hFind);
+
+        if (hFind == INVALID_HANDLE_VALUE ||
+            find.nFileSizeLow != m_items[i].dwFileSize)
+        {
+            // different size
+            lstrcpynW(pszPath, m_items[i].szPath, MAX_PATH);
+            return TRUE;
+        }
+    }
+
+    return FALSE;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// DirWatch --- directory watcher using ReadDirectoryChangesW
+
+static HANDLE s_hThreadAPC = NULL;
+static BOOL s_fTerminateAllWatches = FALSE;
+
+// NOTE: Regard to asynchronous procedure call (APC), please see:
+// https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepex
+
+// The APC thread function for directory watch
+static unsigned __stdcall DirWatchThreadFuncAPC(void *)
+{
+    while (!s_fTerminateAllWatches)
+    {
+#if 1 // FIXME: This is a HACK
+        WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE);
+#else
+        SleepEx(INFINITE, TRUE);
+#endif
+    }
+    return 0;
+}
+
+// the buffer for ReadDirectoryChangesW
+#define BUFFER_SIZE 0x1000
+static BYTE s_buffer[BUFFER_SIZE];
+
+class DirWatch
+{
+public:
+    HANDLE m_hDir;
+    WCHAR m_szDir[MAX_PATH];
+    BOOL m_fDeadWatch;
+    BOOL m_fRecursive;
+    OVERLAPPED m_overlapped; // for async I/O
+    DIRLIST m_DirList;
+
+    static DirWatch *Create(LPCWSTR pszDir, BOOL fSubTree = FALSE);
+    ~DirWatch();
+
+protected:
+    DirWatch(LPCWSTR pszDir, BOOL fSubTree);
+};
+
+DirWatch::DirWatch(LPCWSTR pszDir, BOOL fSubTree)
+    : m_fDeadWatch(FALSE)
+    , m_fRecursive(fSubTree)
+    , m_DirList(pszDir, fSubTree)
+{
+    TRACE("DirWatch::DirWatch: %p, '%S'\n", this, pszDir);
+
+    lstrcpynW(m_szDir, pszDir, MAX_PATH);
+
+    // open the directory to watch changes (for ReadDirectoryChangesW)
+    m_hDir = CreateFileW(pszDir, FILE_LIST_DIRECTORY,
+                         FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                         NULL, OPEN_EXISTING,
+                         FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
+                         NULL);
+}
+
+/*static*/ DirWatch *DirWatch::Create(LPCWSTR pszDir, BOOL fSubTree)
+{
+    WCHAR szFullPath[MAX_PATH];
+    GetFullPathNameW(pszDir, _countof(szFullPath), szFullPath, NULL);
+
+    DirWatch *pDirWatch = new DirWatch(szFullPath, fSubTree);
+    if (pDirWatch->m_hDir == INVALID_HANDLE_VALUE)
+    {
+        ERR("CreateFileW failed\n");
+        delete pDirWatch;
+        pDirWatch = NULL;
+    }
+    return pDirWatch;
+}
+
+DirWatch::~DirWatch()
+{
+    TRACE("DirWatch::~DirWatch: %p, '%S'\n", this, m_szDir);
+
+    if (m_hDir != INVALID_HANDLE_VALUE)
+        CloseHandle(m_hDir);
+}
+
+static BOOL _BeginRead(DirWatch *pDirWatch);
+
+// The APC procedure to add a DirWatch and start the directory watch
+static void NTAPI _AddDirectoryProcAPC(ULONG_PTR Parameter)
+{
+    DirWatch *pDirWatch = (DirWatch *)Parameter;
+    assert(pDirWatch != NULL);
+
+    _BeginRead(pDirWatch);
+}
+
+// The APC procedure to request termination of a DirWatch
+static void NTAPI _RequestTerminationAPC(ULONG_PTR Parameter)
+{
+    DirWatch *pDirWatch = (DirWatch *)Parameter;
+    assert(pDirWatch != NULL);
+
+    pDirWatch->m_fDeadWatch = TRUE;
+    CancelIo(pDirWatch->m_hDir);
+}
+
+// The APC procedure to request termination of all the directory watches
+static void NTAPI _RequestAllTerminationAPC(ULONG_PTR Parameter)
+{
+    s_fTerminateAllWatches = TRUE;
+    CloseHandle(s_hThreadAPC);
+    s_hThreadAPC = NULL;
+}
+
+// convert the file action to an event
+static DWORD
+ConvertActionToEvent(DWORD Action, BOOL fDir)
+{
+    switch (Action)
+    {
+        case FILE_ACTION_ADDED:
+            return (fDir ? SHCNE_MKDIR : SHCNE_CREATE);
+        case FILE_ACTION_REMOVED:
+            return (fDir ? SHCNE_RMDIR : SHCNE_DELETE);
+        case FILE_ACTION_MODIFIED:
+            return (fDir ? SHCNE_UPDATEDIR : SHCNE_UPDATEITEM);
+        case FILE_ACTION_RENAMED_OLD_NAME:
+            break;
+        case FILE_ACTION_RENAMED_NEW_NAME:
+            return (fDir ? SHCNE_RENAMEFOLDER : SHCNE_RENAMEITEM);
+        default:
+            break;
+    }
+    return 0;
+}
+
+// Notify a filesystem notification using pDirWatch.
+static void _ProcessNotification(DirWatch *pDirWatch)
+{
+    PFILE_NOTIFY_INFORMATION pInfo = (PFILE_NOTIFY_INFORMATION)s_buffer;
+    WCHAR szName[MAX_PATH], szPath[MAX_PATH], szTempPath[MAX_PATH];
+    DWORD dwEvent, cbName;
+    BOOL fDir;
+
+    // if the watch is recursive
+    if (pDirWatch->m_fRecursive)
+    {
+        // get the first change
+        if (!pDirWatch->m_DirList.GetFirstChange(szPath))
+            return;
+
+        // then, notify a SHCNE_UPDATEDIR
+        if (lstrcmpiW(pDirWatch->m_szDir, szPath) != 0)
+            PathRemoveFileSpecW(szPath);
+        NotifyFileSystemChange(SHCNE_UPDATEDIR, szPath, NULL);
+
+        // refresh directory list
+        pDirWatch->m_DirList.RemoveAll();
+        pDirWatch->m_DirList.GetDirList(pDirWatch->m_szDir, TRUE);
+        return;
+    }
+
+    // for each entry in s_buffer
+    szPath[0] = szTempPath[0] = 0;
+    for (;;)
+    {
+        // get name (relative from pDirWatch->m_szDir)
+        cbName = pInfo->FileNameLength;
+        if (sizeof(szName) - sizeof(UNICODE_NULL) < cbName)
+        {
+            ERR("pInfo->FileName is longer than szName\n");
+            break;
+        }
+        // NOTE: FILE_NOTIFY_INFORMATION.FileName is not null-terminated.
+        ZeroMemory(szName, sizeof(szName));
+        CopyMemory(szName, pInfo->FileName, cbName);
+
+        // get full path
+        lstrcpynW(szPath, pDirWatch->m_szDir, _countof(szPath));
+        PathAppendW(szPath, szName);
+
+        // convert to long pathname if it contains '~'
+        if (StrChrW(szPath, L'~') != NULL)
+        {
+            GetLongPathNameW(szPath, szName, _countof(szName));
+            lstrcpynW(szPath, szName, _countof(szPath));
+        }
+
+        // convert action to event
+        fDir = PathIsDirectoryW(szPath);
+        dwEvent = ConvertActionToEvent(pInfo->Action, fDir);
+
+        // get the directory list of pDirWatch
+        DIRLIST& List = pDirWatch->m_DirList;
+
+        // convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory
+        if (!fDir && (dwEvent == SHCNE_DELETE) && List.Contains(szPath, TRUE))
+        {
+            fDir = TRUE;
+            dwEvent = SHCNE_RMDIR;
+        }
+
+        // update List
+        switch (dwEvent)
+        {
+            case SHCNE_MKDIR:
+                if (!List.AddItem(szPath, 0, TRUE))
+                    dwEvent = 0;
+                break;
+            case SHCNE_CREATE:
+                if (!List.AddItem(szPath, INVALID_FILE_SIZE, FALSE))
+                    dwEvent = 0;
+                break;
+            case SHCNE_RENAMEFOLDER:
+                if (!List.RenameItem(szTempPath, szPath, TRUE))
+                    dwEvent = 0;
+                break;
+            case SHCNE_RENAMEITEM:
+                if (!List.RenameItem(szTempPath, szPath, FALSE))
+                    dwEvent = 0;
+                break;
+            case SHCNE_RMDIR:
+                if (!List.DeleteItem(szPath, TRUE))
+                    dwEvent = 0;
+                break;
+            case SHCNE_DELETE:
+                if (!List.DeleteItem(szPath, FALSE))
+                    dwEvent = 0;
+                break;
+        }
+
+        if (dwEvent != 0)
+        {
+            // notify
+            if (pInfo->Action == FILE_ACTION_RENAMED_NEW_NAME)
+                NotifyFileSystemChange(dwEvent, szTempPath, szPath);
+            else
+                NotifyFileSystemChange(dwEvent, szPath, NULL);
+        }
+        else if (pInfo->Action == FILE_ACTION_RENAMED_OLD_NAME)
+        {
+            // save path for next FILE_ACTION_RENAMED_NEW_NAME
+            lstrcpynW(szTempPath, szPath, MAX_PATH);
+        }
+
+        if (pInfo->NextEntryOffset == 0)
+            break; // there is no next entry
+
+        // go next entry
+        pInfo = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pInfo + pInfo->NextEntryOffset);
+    }
+}
+
+// The completion routine of ReadDirectoryChangesW.
+static void CALLBACK
+_NotificationCompletion(DWORD dwErrorCode,
+                        DWORD dwNumberOfBytesTransfered,
+                        LPOVERLAPPED lpOverlapped)
+{
+    // MSDN: The hEvent member of the OVERLAPPED structure is not used by the
+    // system in this case, so you can use it yourself. We do just this, storing
+    // a pointer to the working struct in the overlapped structure.
+    DirWatch *pDirWatch = (DirWatch *)lpOverlapped->hEvent;
+    assert(pDirWatch != NULL);
+
+    // If the FSD doesn't support directory change notifications, there's no
+    // no need to retry and requeue notification
+    if (dwErrorCode == ERROR_INVALID_FUNCTION)
+    {
+        ERR("ERROR_INVALID_FUNCTION\n");
+        return;
+    }
+
+    // Also, if the notify operation was canceled (like, user moved to another
+    // directory), then, don't requeue notification.
+    if (dwErrorCode == ERROR_OPERATION_ABORTED)
+    {
+        TRACE("ERROR_OPERATION_ABORTED\n");
+        if (pDirWatch->m_fDeadWatch)
+            delete pDirWatch;
+        return;
+    }
+
+    // is this watch dead?
+    if (pDirWatch->m_fDeadWatch)
+    {
+        TRACE("m_fDeadWatch\n");
+        delete pDirWatch;
+        return;
+    }
+
+    // This likely means overflow, so force whole directory refresh.
+    if (dwNumberOfBytesTransfered == 0)
+    {
+        // do notify a SHCNE_UPDATEDIR
+        NotifyFileSystemChange(SHCNE_UPDATEDIR, pDirWatch->m_szDir, NULL);
+    }
+    else
+    {
+        // do notify
+        _ProcessNotification(pDirWatch);
+    }
+
+    // restart a watch
+    _BeginRead(pDirWatch);
+}
+
+// convert events to notification filter
+static DWORD
+GetFilterFromEvents(DWORD fEvents)
+{
+    // FIXME
+    return (FILE_NOTIFY_CHANGE_FILE_NAME |
+            FILE_NOTIFY_CHANGE_DIR_NAME |
+            FILE_NOTIFY_CHANGE_CREATION |
+            FILE_NOTIFY_CHANGE_SIZE);
+}
+
+// Restart a watch by using ReadDirectoryChangesW function
+static BOOL _BeginRead(DirWatch *pDirWatch)
+{
+    assert(pDirWatch != NULL);
+
+    if (pDirWatch->m_fDeadWatch)
+    {
+        delete pDirWatch;
+        return FALSE; // the watch is dead
+    }
+
+    // initialize the buffer and the overlapped
+    ZeroMemory(s_buffer, sizeof(s_buffer));
+    ZeroMemory(&pDirWatch->m_overlapped, sizeof(pDirWatch->m_overlapped));
+    pDirWatch->m_overlapped.hEvent = (HANDLE)pDirWatch;
+
+    // start the directory watch
+    DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS);
+    if (!ReadDirectoryChangesW(pDirWatch->m_hDir, s_buffer, sizeof(s_buffer),
+                               pDirWatch->m_fRecursive, dwFilter, NULL,
+                               &pDirWatch->m_overlapped, _NotificationCompletion))
+    {
+        ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n",
+            pDirWatch->m_szDir, GetLastError());
+        return FALSE; // failure
+    }
+
+    return TRUE; // success
+}
+
+// create a DirWatch from a REGENTRY
+static DirWatch *
+CreateDirWatchFromRegEntry(LPREGENTRY pRegEntry)
+{
+    if (pRegEntry->ibPidl == 0)
+        return NULL;
+
+    // it must be interrupt level if pRegEntry is a filesystem watch
+    if (!(pRegEntry->fSources & SHCNRF_InterruptLevel))
+        return NULL;
+
+    // get the path
+    WCHAR szPath[MAX_PATH];
+    LPITEMIDLIST pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl);
+    if (!SHGetPathFromIDListW(pidl, szPath) || !PathIsDirectoryW(szPath))
+        return NULL;
+
+    // create a DirWatch
+    DirWatch *pDirWatch = DirWatch::Create(szPath, pRegEntry->fRecursive);
+    if (pDirWatch == NULL)
+        return NULL;
+
+    return pDirWatch;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
 // notification target item
 struct ITEM
 {
@@ -18,6 +626,7 @@ struct ITEM
     DWORD dwUserPID;    // The user PID; that is the process ID of the target window.
     HANDLE hRegEntry;   // The registration entry.
     HWND hwndBroker;    // Client broker window (if any).
+    DirWatch *pDirWatch; // for filesystem notification (for SHCNRF_InterruptLevel)
 };
 
 typedef CWinTraits <
@@ -53,6 +662,7 @@ public:
     LRESULT OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
     LRESULT OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
     LRESULT OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
+    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
 
     DECLARE_NOT_AGGREGATABLE(CChangeNotifyServer)
 
@@ -69,13 +679,15 @@ public:
         MESSAGE_HANDLER(CN_DELIVER_NOTIFICATION, OnDeliverNotification)
         MESSAGE_HANDLER(CN_SUSPEND_RESUME, OnSuspendResume)
         MESSAGE_HANDLER(CN_UNREGISTER_PROCESS, OnRemoveByPID);
+        MESSAGE_HANDLER(WM_DESTROY, OnDestroy);
     END_MSG_MAP()
 
 private:
     UINT m_nNextRegID;
     CSimpleArray<ITEM> m_items;
 
-    BOOL AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry, HWND hwndBroker);
+    BOOL AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry, HWND hwndBroker,
+                 DirWatch *pDirWatch);
     BOOL RemoveItemsByRegID(UINT nRegID, DWORD dwOwnerPID);
     void RemoveItemsByProcess(DWORD dwOwnerPID, DWORD dwUserPID);
     void DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndBroker);
@@ -94,7 +706,8 @@ CChangeNotifyServer::~CChangeNotifyServer()
 {
 }
 
-BOOL CChangeNotifyServer::AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry, HWND hwndBroker)
+BOOL CChangeNotifyServer::AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry,
+                                  HWND hwndBroker, DirWatch *pDirWatch)
 {
     // find the empty room
     for (INT i = 0; i < m_items.GetSize(); ++i)
@@ -106,12 +719,13 @@ BOOL CChangeNotifyServer::AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry
             m_items[i].dwUserPID = dwUserPID;
             m_items[i].hRegEntry = hRegEntry;
             m_items[i].hwndBroker = hwndBroker;
+            m_items[i].pDirWatch = pDirWatch;
             return TRUE;
         }
     }
 
     // no empty room found
-    ITEM item = { nRegID, dwUserPID, hRegEntry, hwndBroker };
+    ITEM item = { nRegID, dwUserPID, hRegEntry, hwndBroker, pDirWatch };
     m_items.Add(item);
     return TRUE;
 }
@@ -119,10 +733,20 @@ BOOL CChangeNotifyServer::AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry
 void CChangeNotifyServer::DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndBroker)
 {
     // destroy broker if any and if first time
-    if (item.hwndBroker && item.hwndBroker != *phwndBroker)
+    HWND hwndBroker = item.hwndBroker;
+    item.hwndBroker = NULL;
+    if (hwndBroker && hwndBroker != *phwndBroker)
+    {
+        ::DestroyWindow(hwndBroker);
+        *phwndBroker = hwndBroker;
+    }
+
+    // request termination of pDirWatch if any
+    DirWatch *pDirWatch = item.pDirWatch;
+    item.pDirWatch = NULL;
+    if (pDirWatch && s_hThreadAPC)
     {
-        ::DestroyWindow(item.hwndBroker);
-        *phwndBroker = item.hwndBroker;
+        QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)pDirWatch);
     }
 
     // free
@@ -131,6 +755,7 @@ void CChangeNotifyServer::DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndB
     item.dwUserPID = 0;
     item.hRegEntry = NULL;
     item.hwndBroker = NULL;
+    item.pDirWatch = NULL;
 }
 
 BOOL CChangeNotifyServer::RemoveItemsByRegID(UINT nRegID, DWORD dwOwnerPID)
@@ -204,11 +829,34 @@ LRESULT CChangeNotifyServer::OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam,
         return FALSE;
     }
 
+    // create a directory watch if necessary
+    DirWatch *pDirWatch = CreateDirWatchFromRegEntry(pRegEntry);
+    if (pDirWatch)
+    {
+        // create an APC thread for directory watching
+        if (s_hThreadAPC == NULL)
+        {
+            unsigned tid;
+            s_fTerminateAllWatches = FALSE;
+            s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirWatchThreadFuncAPC, NULL, 0, &tid);
+            if (s_hThreadAPC == NULL)
+            {
+                pRegEntry->nRegID = INVALID_REG_ID;
+                SHUnlockShared(pRegEntry);
+                delete pDirWatch;
+                return FALSE;
+            }
+        }
+
+        // request adding the watch
+        QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)pDirWatch);
+    }
+
     // unlock the registry entry
     SHUnlockShared(pRegEntry);
 
     // add an ITEM
-    return AddItem(m_nNextRegID, dwUserPID, hNewEntry, hwndBroker);
+    return AddItem(m_nNextRegID, dwUserPID, hNewEntry, hwndBroker, pDirWatch);
 }
 
 // Message CN_UNREGISTER: Unregister registration entries.
@@ -274,6 +922,16 @@ LRESULT CChangeNotifyServer::OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lPar
     return 0;
 }
 
+LRESULT CChangeNotifyServer::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
+{
+    if (s_hThreadAPC)
+    {
+        // request termination of all directory watches
+        QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL);
+    }
+    return 0;
+}
+
 // get next valid registration ID
 UINT CChangeNotifyServer::GetNextRegID()
 {
@@ -344,8 +1002,20 @@ BOOL CChangeNotifyServer::ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntr
     WCHAR szPath[MAX_PATH], szPath1[MAX_PATH], szPath2[MAX_PATH];
     INT cch, cch1, cch2;
 
+    // check fSources
+    if (pTicket->uFlags & SHCNE_INTERRUPT)
+    {
+        if (!(pRegEntry->fSources & SHCNRF_InterruptLevel))
+            return FALSE;
+    }
+    else
+    {
+        if (!(pRegEntry->fSources & SHCNRF_ShellLevel))
+            return FALSE;
+    }
+
     if (pRegEntry->ibPidl == 0)
-        return TRUE;
+        return TRUE; // there is no PIDL
 
     // get the stored pidl
     pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl);