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