[SHELL32] Improve readability of filesystem notification (#2737)

Improve human readability of filesystem change notification.
- Rename the identifiers for human readabilities.
- Split the code into CFilePathList and CDirectoryWatcher classes.
- Encapsulation of code.
CORE-13950
This commit is contained in:
Katayama Hirofumi MZ 2020-06-02 18:42:07 +09:00 committed by GitHub
parent 3dad100ea4
commit 7c9a8c0b89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 783 additions and 642 deletions

View file

@ -6,621 +6,13 @@
*/
#include "shelldesktop.h"
#include "shlwapi_undoc.h"
#include <atlsimpcoll.h> // for CSimpleArray
#include <process.h> // for _beginthreadex
#include "CDirectoryWatcher.h"
#include <assert.h> // for assert
WINE_DEFAULT_DEBUG_CHANNEL(shcn);
// TODO: SHCNRF_RecursiveInterrupt
static inline void
NotifyFileSystemChange(LONG wEventId, LPCWSTR path1, LPCWSTR path2)
{
SHChangeNotify(wEventId | SHCNE_INTERRUPT, SHCNF_PATHW, path1, path2);
}
//////////////////////////////////////////////////////////////////////////////
// DIRLIST --- directory list
// TODO: Share a DIRLIST in multiple Explorer
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
@ -630,7 +22,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)
CDirectoryWatcher *pDirWatch; // for filesystem notification
};
typedef CWinTraits <
@ -691,7 +83,7 @@ private:
CSimpleArray<ITEM> m_items;
BOOL AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry, HWND hwndBroker,
DirWatch *pDirWatch);
CDirectoryWatcher *pDirWatch);
BOOL RemoveItemsByRegID(UINT nRegID, DWORD dwOwnerPID);
void RemoveItemsByProcess(DWORD dwOwnerPID, DWORD dwUserPID);
void DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndBroker);
@ -711,7 +103,7 @@ CChangeNotifyServer::~CChangeNotifyServer()
}
BOOL CChangeNotifyServer::AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry,
HWND hwndBroker, DirWatch *pDirWatch)
HWND hwndBroker, CDirectoryWatcher *pDirWatch)
{
// find the empty room
for (INT i = 0; i < m_items.GetSize(); ++i)
@ -746,12 +138,10 @@ void CChangeNotifyServer::DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndB
}
// request termination of pDirWatch if any
DirWatch *pDirWatch = item.pDirWatch;
CDirectoryWatcher *pDirWatch = item.pDirWatch;
item.pDirWatch = NULL;
if (pDirWatch && s_hThreadAPC)
{
QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)pDirWatch);
}
if (pDirWatch)
pDirWatch->RequestTermination();
// free
SHFreeShared(item.hRegEntry, dwOwnerPID);
@ -791,15 +181,29 @@ void CChangeNotifyServer::RemoveItemsByProcess(DWORD dwOwnerPID, DWORD dwUserPID
}
}
static BOOL CreateAPCThread(void)
// create a CDirectoryWatcher from a REGENTRY
static CDirectoryWatcher *
CreateDirectoryWatcherFromRegEntry(LPREGENTRY pRegEntry)
{
if (s_hThreadAPC != NULL)
return TRUE;
if (pRegEntry->ibPidl == 0)
return NULL;
unsigned tid;
s_fTerminateAllWatches = FALSE;
s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirWatchThreadFuncAPC, NULL, 0, &tid);
return s_hThreadAPC != 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 CDirectoryWatcher
CDirectoryWatcher *pDirectoryWatcher = CDirectoryWatcher::Create(szPath, pRegEntry->fRecursive);
if (pDirectoryWatcher == NULL)
return NULL;
return pDirectoryWatcher;
}
// Message CN_REGISTER: Register the registration entry.
@ -845,20 +249,13 @@ LRESULT CChangeNotifyServer::OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam,
}
// create a directory watch if necessary
DirWatch *pDirWatch = CreateDirWatchFromRegEntry(pRegEntry);
if (pDirWatch)
CDirectoryWatcher *pDirWatch = CreateDirectoryWatcherFromRegEntry(pRegEntry);
if (pDirWatch && !pDirWatch->RequestAddWatcher())
{
// create an APC thread for directory watching
if (!CreateAPCThread())
{
pRegEntry->nRegID = INVALID_REG_ID;
SHUnlockShared(pRegEntry);
delete pDirWatch;
return FALSE;
}
// request adding the watch
QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)pDirWatch);
pRegEntry->nRegID = INVALID_REG_ID;
SHUnlockShared(pRegEntry);
delete pDirWatch;
return FALSE;
}
// unlock the registry entry
@ -933,11 +330,7 @@ LRESULT CChangeNotifyServer::OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lPar
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);
}
CDirectoryWatcher::RequestAllWatchersTermination();
return 0;
}

View file

@ -0,0 +1,406 @@
/*
* PROJECT: shell32
* LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
* PURPOSE: Shell change notification
* COPYRIGHT: Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
*/
#include "shelldesktop.h"
#include "CDirectoryWatcher.h"
#include <process.h> // for _beginthreadex
#include <assert.h> // for assert
WINE_DEFAULT_DEBUG_CHANNEL(shcn);
// Notify filesystem change
static inline void
NotifyFileSystemChange(LONG wEventId, LPCWSTR path1, LPCWSTR path2)
{
SHChangeNotify(wEventId | SHCNE_INTERRUPT, SHCNF_PATHW, path1, path2);
}
// The handle of the APC thread
static HANDLE s_hThreadAPC = NULL;
// Terminate now?
static BOOL s_fTerminateAllWatchers = FALSE;
// the buffer for ReadDirectoryChangesW
#define BUFFER_SIZE 0x1000
static BYTE s_buffer[BUFFER_SIZE];
// The APC thread function for directory watch
static unsigned __stdcall DirectoryWatcherThreadFuncAPC(void *)
{
while (!s_fTerminateAllWatchers)
{
#if 1 // FIXME: This is a HACK
WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE);
#else
SleepEx(INFINITE, TRUE);
#endif
}
return 0;
}
// The APC procedure to add a CDirectoryWatcher and start the directory watch
static void NTAPI _AddDirectoryProcAPC(ULONG_PTR Parameter)
{
CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)Parameter;
assert(pDirectoryWatcher != NULL);
pDirectoryWatcher->RestartWatching();
}
// The APC procedure to request termination of a CDirectoryWatcher
static void NTAPI _RequestTerminationAPC(ULONG_PTR Parameter)
{
CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)Parameter;
assert(pDirectoryWatcher != NULL);
pDirectoryWatcher->QuitWatching();
}
// The APC procedure to request termination of all the directory watches
static void NTAPI _RequestAllTerminationAPC(ULONG_PTR Parameter)
{
s_fTerminateAllWatchers = TRUE;
CloseHandle(s_hThreadAPC);
s_hThreadAPC = NULL;
}
CDirectoryWatcher::CDirectoryWatcher(LPCWSTR pszDirectoryPath, BOOL fSubTree)
: m_fDead(FALSE)
, m_fRecursive(fSubTree)
, m_file_list(pszDirectoryPath, fSubTree)
{
TRACE("CDirectoryWatcher::CDirectoryWatcher: %p, '%S'\n", this, pszDirectoryPath);
lstrcpynW(m_szDirectoryPath, pszDirectoryPath, MAX_PATH);
// open the directory to watch changes (for ReadDirectoryChangesW)
m_hDirectory = CreateFileW(pszDirectoryPath, FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
NULL);
}
/*static*/ CDirectoryWatcher *
CDirectoryWatcher::Create(LPCWSTR pszDirectoryPath, BOOL fSubTree)
{
WCHAR szFullPath[MAX_PATH];
GetFullPathNameW(pszDirectoryPath, _countof(szFullPath), szFullPath, NULL);
CDirectoryWatcher *pDirectoryWatcher = new CDirectoryWatcher(szFullPath, fSubTree);
if (pDirectoryWatcher->m_hDirectory == INVALID_HANDLE_VALUE)
{
ERR("CreateFileW failed\n");
delete pDirectoryWatcher;
pDirectoryWatcher = NULL;
}
return pDirectoryWatcher;
}
CDirectoryWatcher::~CDirectoryWatcher()
{
TRACE("CDirectoryWatcher::~CDirectoryWatcher: %p, '%S'\n", this, m_szDirectoryPath);
if (m_hDirectory != INVALID_HANDLE_VALUE)
CloseHandle(m_hDirectory);
}
// 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 pDirectoryWatcher.
void CDirectoryWatcher::ProcessNotification()
{
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 (m_fRecursive)
{
// get the first change
if (!m_file_list.GetFirstChange(szPath))
return;
// then, notify a SHCNE_UPDATEDIR
if (lstrcmpiW(m_szDirectoryPath, szPath) != 0)
PathRemoveFileSpecW(szPath);
NotifyFileSystemChange(SHCNE_UPDATEDIR, szPath, NULL);
// refresh directory list
m_file_list.RemoveAll();
m_file_list.AddPathsFromDirectory(m_szDirectoryPath, TRUE);
return;
}
// for each entry in s_buffer
szPath[0] = szTempPath[0] = 0;
for (;;)
{
// get name (relative from m_szDirectoryPath)
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, m_szDirectoryPath, _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);
// convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory
if (!fDir && (dwEvent == SHCNE_DELETE) && m_file_list.ContainsPath(szPath, TRUE))
{
fDir = TRUE;
dwEvent = SHCNE_RMDIR;
}
// update m_file_list
switch (dwEvent)
{
case SHCNE_MKDIR:
if (!m_file_list.AddPath(szPath, 0, TRUE))
dwEvent = 0;
break;
case SHCNE_CREATE:
if (!m_file_list.AddPath(szPath, INVALID_FILE_SIZE, FALSE))
dwEvent = 0;
break;
case SHCNE_RENAMEFOLDER:
if (!m_file_list.RenamePath(szTempPath, szPath, TRUE))
dwEvent = 0;
break;
case SHCNE_RENAMEITEM:
if (!m_file_list.RenamePath(szTempPath, szPath, FALSE))
dwEvent = 0;
break;
case SHCNE_RMDIR:
if (!m_file_list.DeletePath(szPath, TRUE))
dwEvent = 0;
break;
case SHCNE_DELETE:
if (!m_file_list.DeletePath(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);
}
}
void CDirectoryWatcher::ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered)
{
// 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 (IsDead())
delete this;
return;
}
// is this watch dead?
if (IsDead())
{
TRACE("IsDead()\n");
delete this;
return;
}
// This likely means overflow, so force whole directory refresh.
if (dwNumberOfBytesTransfered == 0)
{
// do notify a SHCNE_UPDATEDIR
NotifyFileSystemChange(SHCNE_UPDATEDIR, m_szDirectoryPath, NULL);
}
else
{
// do notify
ProcessNotification();
}
// restart a watch
RestartWatching();
}
// 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.
CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)lpOverlapped->hEvent;
assert(pDirectoryWatcher != NULL);
pDirectoryWatcher->ReadCompletion(dwErrorCode, dwNumberOfBytesTransfered);
}
// 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
BOOL CDirectoryWatcher::RestartWatching()
{
assert(this != NULL);
if (IsDead())
{
delete this;
return FALSE; // the watch is dead
}
// initialize the buffer and the overlapped
ZeroMemory(s_buffer, sizeof(s_buffer));
ZeroMemory(&m_overlapped, sizeof(m_overlapped));
m_overlapped.hEvent = (HANDLE)this;
// start the directory watch
DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS);
if (!ReadDirectoryChangesW(m_hDirectory, s_buffer, sizeof(s_buffer),
m_fRecursive, dwFilter, NULL,
&m_overlapped, _NotificationCompletion))
{
ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n",
m_szDirectoryPath, GetLastError());
return FALSE; // failure
}
return TRUE; // success
}
BOOL CDirectoryWatcher::CreateAPCThread()
{
if (s_hThreadAPC != NULL)
return TRUE;
unsigned tid;
s_fTerminateAllWatchers = FALSE;
s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirectoryWatcherThreadFuncAPC,
NULL, 0, &tid);
return s_hThreadAPC != NULL;
}
BOOL CDirectoryWatcher::RequestAddWatcher()
{
assert(this != NULL);
// create an APC thread for directory watching
if (!CreateAPCThread())
return FALSE;
// request adding the watch
QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)this);
return TRUE;
}
BOOL CDirectoryWatcher::RequestTermination()
{
assert(this != NULL);
if (s_hThreadAPC)
{
QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)this);
return TRUE;
}
return FALSE;
}
/*static*/ void CDirectoryWatcher::RequestAllWatchersTermination()
{
if (!s_hThreadAPC)
return;
// request termination of all directory watches
QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL);
}
void CDirectoryWatcher::QuitWatching()
{
assert(this != NULL);
m_fDead = TRUE;
CancelIo(m_hDirectory);
}
BOOL CDirectoryWatcher::IsDead() const
{
return m_fDead;
}

View file

@ -0,0 +1,40 @@
/*
* PROJECT: shell32
* LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
* PURPOSE: Shell change notification
* COPYRIGHT: Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
*/
#pragma once
#include "CFilePathList.h"
// NOTE: Regard to asynchronous procedure call (APC), please see:
// https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-sleepex
class CDirectoryWatcher
{
public:
HANDLE m_hDirectory;
WCHAR m_szDirectoryPath[MAX_PATH];
static CDirectoryWatcher *Create(LPCWSTR pszDirectoryPath, BOOL fSubTree);
static void RequestAllWatchersTermination();
~CDirectoryWatcher();
BOOL IsDead() const;
BOOL RestartWatching();
void QuitWatching();
BOOL RequestAddWatcher();
BOOL RequestTermination();
void ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered);
protected:
BOOL m_fDead;
BOOL m_fRecursive;
CFilePathList m_file_list;
OVERLAPPED m_overlapped;
BOOL CreateAPCThread();
void ProcessNotification();
CDirectoryWatcher(LPCWSTR pszDirectoryPath, BOOL fSubTree);
};

View file

@ -0,0 +1,192 @@
/*
* PROJECT: shell32
* LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
* PURPOSE: Shell change notification
* COPYRIGHT: Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
*/
#include "shelldesktop.h"
#include "CFilePathList.h"
#include <assert.h> // for assert
WINE_DEFAULT_DEBUG_CHANNEL(shcn);
BOOL CFilePathList::ContainsPath(LPCWSTR pszPath, BOOL fIsDirectory) const
{
assert(!PathIsRelativeW(pszPath));
for (INT i = 0; i < m_items.GetSize(); ++i)
{
if (m_items[i].IsEmpty() || fIsDirectory != m_items[i].IsDirectory())
continue;
if (m_items[i].EqualPath(pszPath))
return TRUE; // matched
}
return FALSE;
}
static DWORD GetSizeOfFile(LPCWSTR pszPath)
{
WIN32_FIND_DATAW find;
HANDLE hFind = FindFirstFileW(pszPath, &find);
if (hFind == INVALID_HANDLE_VALUE)
return FALSE;
FindClose(hFind);
return find.nFileSizeLow;
}
BOOL CFilePathList::AddPath(LPCWSTR pszPath, DWORD dwFileSize, BOOL fIsDirectory)
{
assert(!PathIsRelativeW(pszPath));
if (dwFileSize == INVALID_FILE_SIZE)
{
dwFileSize = GetSizeOfFile(pszPath);
}
CFilePathItem item(pszPath, dwFileSize, fIsDirectory);
return m_items.Add(item);
}
BOOL CFilePathList::RenamePath(LPCWSTR pszPath1, LPCWSTR pszPath2, BOOL fIsDirectory)
{
assert(!PathIsRelativeW(pszPath1));
assert(!PathIsRelativeW(pszPath2));
for (INT i = 0; i < m_items.GetSize(); ++i)
{
if (m_items[i].IsDirectory() == fIsDirectory && m_items[i].EqualPath(pszPath1))
{
// matched
m_items[i].SetPath(pszPath2);
return TRUE;
}
}
return FALSE;
}
BOOL CFilePathList::DeletePath(LPCWSTR pszPath, BOOL fIsDirectory)
{
assert(!PathIsRelativeW(pszPath));
for (INT i = 0; i < m_items.GetSize(); ++i)
{
if (m_items[i].IsDirectory() == fIsDirectory && m_items[i].EqualPath(pszPath))
{
// matched
m_items[i].SetPath(NULL);
return TRUE;
}
}
return FALSE;
}
BOOL CFilePathList::AddPathsFromDirectory(LPCWSTR pszDirectoryPath, BOOL fRecursive)
{
// get the full path
WCHAR szPath[MAX_PATH];
lstrcpynW(szPath, pszDirectoryPath, _countof(szPath));
assert(!PathIsRelativeW(szPath));
// is it a directory?
if (!PathIsDirectoryW(szPath))
return FALSE;
// add the path
if (!AddPath(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);
BOOL fIsDirectory = !!(find.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
// add the path and do recurse
if (fRecursive && fIsDirectory)
AddPathsFromDirectory(szPath, fRecursive);
else
AddPath(szPath, find.nFileSizeLow, fIsDirectory);
} while (FindNextFileW(hFind, &find));
FindClose(hFind);
return TRUE;
}
BOOL CFilePathList::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].IsDirectory()) // item is a directory
{
if (!PathIsDirectoryW(m_items[i].GetPath()))
{
// mismatched
lstrcpynW(pszPath, m_items[i].GetPath(), MAX_PATH);
return TRUE;
}
}
else // item is a normal file
{
if (!PathFileExistsW(m_items[i].GetPath()) ||
PathIsDirectoryW(m_items[i].GetPath()))
{
// mismatched
lstrcpynW(pszPath, m_items[i].GetPath(), 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].IsDirectory())
continue;
// get size
hFind = FindFirstFileW(m_items[i].GetPath(), &find);
FindClose(hFind);
if (hFind == INVALID_HANDLE_VALUE ||
find.nFileSizeLow != m_items[i].GetSize())
{
// different size
lstrcpynW(pszPath, m_items[i].GetPath(), MAX_PATH);
return TRUE;
}
}
return FALSE;
}

View file

@ -0,0 +1,108 @@
#pragma once
#include <atlsimpcoll.h> // for CSimpleArray
//////////////////////////////////////////////////////////////////////////////
// A pathname with info
class CFilePathItem
{
public:
CFilePathItem() : m_pszPath(NULL)
{
}
CFilePathItem(LPCWSTR pszPath, DWORD dwFileSize, BOOL IsDirectory)
{
m_pszPath = _wcsdup(pszPath);
m_dwFileSize = dwFileSize;
m_fIsDirectory = IsDirectory;
}
CFilePathItem(const CFilePathItem& item)
: m_pszPath(_wcsdup(item.m_pszPath))
, m_dwFileSize(item.m_dwFileSize)
, m_fIsDirectory(item.m_fIsDirectory)
{
}
CFilePathItem& operator=(const CFilePathItem& item)
{
free(m_pszPath);
m_pszPath = _wcsdup(item.m_pszPath);
m_dwFileSize = item.m_dwFileSize;
m_fIsDirectory = item.m_fIsDirectory;
return *this;
}
~CFilePathItem()
{
free(m_pszPath);
}
BOOL IsEmpty() const
{
return m_pszPath == NULL;
}
BOOL IsDirectory() const
{
return m_fIsDirectory;
}
LPCWSTR GetPath() const
{
return m_pszPath;
}
void SetPath(LPCWSTR pszPath)
{
free(m_pszPath);
m_pszPath = _wcsdup(pszPath);
}
BOOL EqualPath(LPCWSTR pszPath) const
{
return m_pszPath != NULL && lstrcmpiW(m_pszPath, pszPath) == 0;
}
DWORD GetSize() const
{
return m_dwFileSize;
}
protected:
LPWSTR m_pszPath; // A full path, malloc'ed
DWORD m_dwFileSize; // the size of a file
BOOL m_fIsDirectory; // is it a directory?
};
// the file list
class CFilePathList
{
public:
CFilePathList()
{
}
CFilePathList(LPCWSTR pszDirectoryPath, BOOL fRecursive)
{
AddPathsFromDirectory(pszDirectoryPath, fRecursive);
}
BOOL ContainsPath(LPCWSTR pszPath, BOOL fIsDirectory) const;
BOOL GetFirstChange(LPWSTR pszPath) const;
BOOL AddPath(LPCWSTR pszPath, DWORD dwFileSize, BOOL fIsDirectory);
BOOL AddPathsFromDirectory(LPCWSTR pszDirectoryPath, BOOL fRecursive);
BOOL RenamePath(LPCWSTR pszPath1, LPCWSTR pszPath2, BOOL fIsDirectory);
BOOL DeletePath(LPCWSTR pszPath, BOOL fIsDirectory);
void RemoveAll()
{
m_items.RemoveAll();
}
protected:
CSimpleArray<CFilePathItem> m_items;
};

View file

@ -12,6 +12,8 @@ include_directories(${REACTOS_SOURCE_DIR}/sdk/lib/atl)
list(APPEND SOURCE
CChangeNotifyServer.cpp
CDesktopBrowser.cpp
CDirectoryWatcher.cpp
CFilePathList.cpp
dde.cpp)
add_library(shelldesktop ${SOURCE})