reactos/dll/win32/shell32/shelldesktop/CDirectoryWatcher.cpp
Katayama Hirofumi MZ 8204c6e2fd
[SHELL32] CDirectoryWatcher: Check if window is alive (#6915)
Kill the zombie watchers for system
performance.
JIRA issue: CORE-13950
- Add HWND parameter to
  CDirectoryWatcher::Create.
- Check whether the HWND is alive at
  CDirectoryWatcher::IsDead.
2024-05-22 20:33:26 +09:00

400 lines
12 KiB
C++

/*
* 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 | SHCNF_FLUSH, 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(HWND hNotifyWnd, LPCWSTR pszDirectoryPath, BOOL fSubTree)
: m_hNotifyWnd(hNotifyWnd)
, m_fDead(FALSE)
, m_fRecursive(fSubTree)
, m_dir_list(pszDirectoryPath, fSubTree)
{
TRACE("%p, '%S'\n", this, pszDirectoryPath);
GetFullPathNameW(pszDirectoryPath, _countof(m_szDirectoryPath), m_szDirectoryPath, NULL);
// open the directory to watch changes (for ReadDirectoryChangesW)
m_hDirectory = CreateFileW(m_szDirectoryPath, 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(HWND hNotifyWnd, LPCWSTR pszDirectoryPath, BOOL fSubTree)
{
CDirectoryWatcher *pDirectoryWatcher =
new CDirectoryWatcher(hNotifyWnd, pszDirectoryPath, fSubTree);
if (pDirectoryWatcher->m_hDirectory == INVALID_HANDLE_VALUE)
{
ERR("CreateFileW failed\n");
delete pDirectoryWatcher;
pDirectoryWatcher = NULL;
}
return pDirectoryWatcher;
}
CDirectoryWatcher::~CDirectoryWatcher()
{
TRACE("%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;
TRACE("CDirectoryWatcher::ProcessNotification: enter\n");
// 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)
{
if (GetLongPathNameW(szPath, szName, _countof(szName)) &&
!PathIsRelativeW(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_dir_list.ContainsPath(szPath))
{
fDir = TRUE;
dwEvent = SHCNE_RMDIR;
}
// update m_dir_list
switch (dwEvent)
{
case SHCNE_MKDIR:
if (!PathIsDirectoryW(szPath) || !m_dir_list.AddPath(szPath))
dwEvent = 0;
break;
case SHCNE_CREATE:
if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
dwEvent = 0;
break;
case SHCNE_RENAMEFOLDER:
if (!PathIsDirectoryW(szPath) || !m_dir_list.RenamePath(szTempPath, szPath))
dwEvent = 0;
break;
case SHCNE_RENAMEITEM:
if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath))
dwEvent = 0;
break;
case SHCNE_RMDIR:
if (PathIsDirectoryW(szPath) || !m_dir_list.DeletePath(szPath))
dwEvent = 0;
break;
case SHCNE_DELETE:
if (PathFileExistsW(szPath))
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);
}
TRACE("CDirectoryWatcher::ProcessNotification: leave\n");
}
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;
m_hNotifyWnd = NULL;
CancelIo(m_hDirectory);
}
BOOL CDirectoryWatcher::IsDead()
{
if (m_hNotifyWnd && !::IsWindow(m_hNotifyWnd))
{
m_hNotifyWnd = NULL;
m_fDead = TRUE;
CancelIo(m_hDirectory);
}
return m_fDead;
}