Reactos
at master 401 lines 12 kB view raw
1/* 2 * PROJECT: shell32 3 * LICENSE: LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later) 4 * PURPOSE: Shell change notification 5 * COPYRIGHT: Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com) 6 */ 7#include "shelldesktop.h" 8#include "CDirectoryWatcher.h" 9#include <process.h> // for _beginthreadex 10#include <assert.h> // for assert 11 12WINE_DEFAULT_DEBUG_CHANNEL(shcn); 13 14// Notify filesystem change 15static inline void 16NotifyFileSystemChange(LONG wEventId, LPCWSTR path1, LPCWSTR path2) 17{ 18 SHChangeNotify(wEventId | SHCNE_INTERRUPT, SHCNF_PATHW | SHCNF_FLUSH, path1, path2); 19} 20 21// The handle of the APC thread 22static HANDLE s_hThreadAPC = NULL; 23 24// Terminate now? 25static BOOL s_fTerminateAllWatchers = FALSE; 26 27// the buffer for ReadDirectoryChangesW 28#define BUFFER_SIZE 0x1000 29static BYTE s_buffer[BUFFER_SIZE]; 30 31// The APC thread function for directory watch 32static unsigned __stdcall DirectoryWatcherThreadFuncAPC(void *) 33{ 34 while (!s_fTerminateAllWatchers) 35 { 36#if 1 // FIXME: This is a HACK 37 WaitForSingleObjectEx(GetCurrentThread(), INFINITE, TRUE); 38#else 39 SleepEx(INFINITE, TRUE); 40#endif 41 } 42 return 0; 43} 44 45// The APC procedure to add a CDirectoryWatcher and start the directory watch 46static void NTAPI _AddDirectoryProcAPC(ULONG_PTR Parameter) 47{ 48 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)Parameter; 49 assert(pDirectoryWatcher != NULL); 50 51 pDirectoryWatcher->RestartWatching(); 52} 53 54// The APC procedure to request termination of a CDirectoryWatcher 55static void NTAPI _RequestTerminationAPC(ULONG_PTR Parameter) 56{ 57 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)Parameter; 58 assert(pDirectoryWatcher != NULL); 59 60 pDirectoryWatcher->QuitWatching(); 61} 62 63// The APC procedure to request termination of all the directory watches 64static void NTAPI _RequestAllTerminationAPC(ULONG_PTR Parameter) 65{ 66 s_fTerminateAllWatchers = TRUE; 67 CloseHandle(s_hThreadAPC); 68 s_hThreadAPC = NULL; 69} 70 71CDirectoryWatcher::CDirectoryWatcher(HWND hNotifyWnd, LPCWSTR pszDirectoryPath, BOOL fSubTree) 72 : m_hNotifyWnd(hNotifyWnd) 73 , m_fDead(FALSE) 74 , m_fRecursive(fSubTree) 75 , m_dir_list(pszDirectoryPath, fSubTree) 76{ 77 TRACE("%p, '%S'\n", this, pszDirectoryPath); 78 79 GetFullPathNameW(pszDirectoryPath, _countof(m_szDirectoryPath), m_szDirectoryPath, NULL); 80 81 // open the directory to watch changes (for ReadDirectoryChangesW) 82 m_hDirectory = CreateFileW(m_szDirectoryPath, FILE_LIST_DIRECTORY, 83 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 84 NULL, OPEN_EXISTING, 85 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, 86 NULL); 87} 88 89/*static*/ CDirectoryWatcher * 90CDirectoryWatcher::Create(HWND hNotifyWnd, LPCWSTR pszDirectoryPath, BOOL fSubTree) 91{ 92 CDirectoryWatcher *pDirectoryWatcher = 93 new CDirectoryWatcher(hNotifyWnd, pszDirectoryPath, fSubTree); 94 if (pDirectoryWatcher->m_hDirectory == INVALID_HANDLE_VALUE) 95 { 96 ERR("CreateFileW failed\n"); 97 delete pDirectoryWatcher; 98 pDirectoryWatcher = NULL; 99 } 100 return pDirectoryWatcher; 101} 102 103CDirectoryWatcher::~CDirectoryWatcher() 104{ 105 TRACE("%p, '%S'\n", this, m_szDirectoryPath); 106 107 if (m_hDirectory != INVALID_HANDLE_VALUE) 108 CloseHandle(m_hDirectory); 109} 110 111// convert the file action to an event 112static DWORD 113ConvertActionToEvent(DWORD Action, BOOL fDir) 114{ 115 switch (Action) 116 { 117 case FILE_ACTION_ADDED: 118 return (fDir ? SHCNE_MKDIR : SHCNE_CREATE); 119 case FILE_ACTION_REMOVED: 120 return (fDir ? SHCNE_RMDIR : SHCNE_DELETE); 121 case FILE_ACTION_MODIFIED: 122 return (fDir ? SHCNE_UPDATEDIR : SHCNE_UPDATEITEM); 123 case FILE_ACTION_RENAMED_OLD_NAME: 124 break; 125 case FILE_ACTION_RENAMED_NEW_NAME: 126 return (fDir ? SHCNE_RENAMEFOLDER : SHCNE_RENAMEITEM); 127 default: 128 break; 129 } 130 return 0; 131} 132 133// Notify a filesystem notification using pDirectoryWatcher. 134void CDirectoryWatcher::ProcessNotification() 135{ 136 PFILE_NOTIFY_INFORMATION pInfo = (PFILE_NOTIFY_INFORMATION)s_buffer; 137 WCHAR szName[MAX_PATH], szPath[MAX_PATH], szTempPath[MAX_PATH]; 138 DWORD dwEvent, cbName; 139 BOOL fDir; 140 TRACE("CDirectoryWatcher::ProcessNotification: enter\n"); 141 142 // for each entry in s_buffer 143 szPath[0] = szTempPath[0] = 0; 144 for (;;) 145 { 146 // get name (relative from m_szDirectoryPath) 147 cbName = pInfo->FileNameLength; 148 if (sizeof(szName) - sizeof(UNICODE_NULL) < cbName) 149 { 150 ERR("pInfo->FileName is longer than szName\n"); 151 break; 152 } 153 // NOTE: FILE_NOTIFY_INFORMATION.FileName is not null-terminated. 154 ZeroMemory(szName, sizeof(szName)); 155 CopyMemory(szName, pInfo->FileName, cbName); 156 157 // get full path 158 lstrcpynW(szPath, m_szDirectoryPath, _countof(szPath)); 159 PathAppendW(szPath, szName); 160 161 // convert to long pathname if it contains '~' 162 if (StrChrW(szPath, L'~') != NULL) 163 { 164 if (GetLongPathNameW(szPath, szName, _countof(szName)) && 165 !PathIsRelativeW(szName)) 166 { 167 lstrcpynW(szPath, szName, _countof(szPath)); 168 } 169 } 170 171 // convert action to event 172 fDir = PathIsDirectoryW(szPath); 173 dwEvent = ConvertActionToEvent(pInfo->Action, fDir); 174 175 // convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory 176 if (!fDir && (dwEvent == SHCNE_DELETE) && m_dir_list.ContainsPath(szPath)) 177 { 178 fDir = TRUE; 179 dwEvent = SHCNE_RMDIR; 180 } 181 182 // update m_dir_list 183 switch (dwEvent) 184 { 185 case SHCNE_MKDIR: 186 if (!PathIsDirectoryW(szPath) || !m_dir_list.AddPath(szPath)) 187 dwEvent = 0; 188 break; 189 case SHCNE_CREATE: 190 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath)) 191 dwEvent = 0; 192 break; 193 case SHCNE_RENAMEFOLDER: 194 if (!PathIsDirectoryW(szPath) || !m_dir_list.RenamePath(szTempPath, szPath)) 195 dwEvent = 0; 196 break; 197 case SHCNE_RENAMEITEM: 198 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath)) 199 dwEvent = 0; 200 break; 201 case SHCNE_RMDIR: 202 if (PathIsDirectoryW(szPath) || !m_dir_list.DeletePath(szPath)) 203 dwEvent = 0; 204 break; 205 case SHCNE_DELETE: 206 if (PathFileExistsW(szPath)) 207 dwEvent = 0; 208 break; 209 } 210 211 if (dwEvent != 0) 212 { 213 // notify 214 if (pInfo->Action == FILE_ACTION_RENAMED_NEW_NAME) 215 NotifyFileSystemChange(dwEvent, szTempPath, szPath); 216 else 217 NotifyFileSystemChange(dwEvent, szPath, NULL); 218 } 219 else if (pInfo->Action == FILE_ACTION_RENAMED_OLD_NAME) 220 { 221 // save path for next FILE_ACTION_RENAMED_NEW_NAME 222 lstrcpynW(szTempPath, szPath, MAX_PATH); 223 } 224 225 if (pInfo->NextEntryOffset == 0) 226 break; // there is no next entry 227 228 // go next entry 229 pInfo = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pInfo + pInfo->NextEntryOffset); 230 } 231 232 TRACE("CDirectoryWatcher::ProcessNotification: leave\n"); 233} 234 235void CDirectoryWatcher::ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered) 236{ 237 // If the FSD doesn't support directory change notifications, there's no 238 // no need to retry and requeue notification 239 if (dwErrorCode == ERROR_INVALID_FUNCTION) 240 { 241 ERR("ERROR_INVALID_FUNCTION\n"); 242 return; 243 } 244 245 // Also, if the notify operation was canceled (like, user moved to another 246 // directory), then, don't requeue notification. 247 if (dwErrorCode == ERROR_OPERATION_ABORTED) 248 { 249 TRACE("ERROR_OPERATION_ABORTED\n"); 250 if (IsDead()) 251 delete this; 252 return; 253 } 254 255 // is this watch dead? 256 if (IsDead()) 257 { 258 TRACE("IsDead()\n"); 259 delete this; 260 return; 261 } 262 263 // This likely means overflow, so force whole directory refresh. 264 if (dwNumberOfBytesTransfered == 0) 265 { 266 // do notify a SHCNE_UPDATEDIR 267 NotifyFileSystemChange(SHCNE_UPDATEDIR, m_szDirectoryPath, NULL); 268 } 269 else 270 { 271 // do notify 272 ProcessNotification(); 273 } 274 275 // restart a watch 276 RestartWatching(); 277} 278 279// The completion routine of ReadDirectoryChangesW. 280static void CALLBACK 281_NotificationCompletion(DWORD dwErrorCode, 282 DWORD dwNumberOfBytesTransfered, 283 LPOVERLAPPED lpOverlapped) 284{ 285 // MSDN: The hEvent member of the OVERLAPPED structure is not used by the 286 // system in this case, so you can use it yourself. We do just this, storing 287 // a pointer to the working struct in the overlapped structure. 288 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)lpOverlapped->hEvent; 289 assert(pDirectoryWatcher != NULL); 290 291 pDirectoryWatcher->ReadCompletion(dwErrorCode, dwNumberOfBytesTransfered); 292} 293 294// convert events to notification filter 295static DWORD 296GetFilterFromEvents(DWORD fEvents) 297{ 298 // FIXME 299 return (FILE_NOTIFY_CHANGE_FILE_NAME | 300 FILE_NOTIFY_CHANGE_DIR_NAME | 301 FILE_NOTIFY_CHANGE_CREATION | 302 FILE_NOTIFY_CHANGE_SIZE | 303 FILE_NOTIFY_CHANGE_ATTRIBUTES); 304} 305 306// Restart a watch by using ReadDirectoryChangesW function 307BOOL CDirectoryWatcher::RestartWatching() 308{ 309 assert(this != NULL); 310 311 if (IsDead()) 312 { 313 delete this; 314 return FALSE; // the watch is dead 315 } 316 317 // initialize the buffer and the overlapped 318 ZeroMemory(s_buffer, sizeof(s_buffer)); 319 ZeroMemory(&m_overlapped, sizeof(m_overlapped)); 320 m_overlapped.hEvent = (HANDLE)this; 321 322 // start the directory watch 323 DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS); 324 if (!ReadDirectoryChangesW(m_hDirectory, s_buffer, sizeof(s_buffer), 325 m_fRecursive, dwFilter, NULL, 326 &m_overlapped, _NotificationCompletion)) 327 { 328 ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n", 329 m_szDirectoryPath, GetLastError()); 330 return FALSE; // failure 331 } 332 333 return TRUE; // success 334} 335 336BOOL CDirectoryWatcher::CreateAPCThread() 337{ 338 if (s_hThreadAPC != NULL) 339 return TRUE; 340 341 unsigned tid; 342 s_fTerminateAllWatchers = FALSE; 343 s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirectoryWatcherThreadFuncAPC, 344 NULL, 0, &tid); 345 return s_hThreadAPC != NULL; 346} 347 348BOOL CDirectoryWatcher::RequestAddWatcher() 349{ 350 assert(this != NULL); 351 352 // create an APC thread for directory watching 353 if (!CreateAPCThread()) 354 return FALSE; 355 356 // request adding the watch 357 QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)this); 358 return TRUE; 359} 360 361BOOL CDirectoryWatcher::RequestTermination() 362{ 363 assert(this != NULL); 364 365 if (s_hThreadAPC) 366 { 367 QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)this); 368 return TRUE; 369 } 370 371 return FALSE; 372} 373 374/*static*/ void CDirectoryWatcher::RequestAllWatchersTermination() 375{ 376 if (!s_hThreadAPC) 377 return; 378 379 // request termination of all directory watches 380 QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL); 381} 382 383void CDirectoryWatcher::QuitWatching() 384{ 385 assert(this != NULL); 386 387 m_fDead = TRUE; 388 m_hNotifyWnd = NULL; 389 CancelIo(m_hDirectory); 390} 391 392BOOL CDirectoryWatcher::IsDead() 393{ 394 if (m_hNotifyWnd && !::IsWindow(m_hNotifyWnd)) 395 { 396 m_hNotifyWnd = NULL; 397 m_fDead = TRUE; 398 CancelIo(m_hDirectory); 399 } 400 return m_fDead; 401}