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 | SHCNF_FLUSH, 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(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 * 90 CDirectoryWatcher::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 103 CDirectoryWatcher::~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 112 static DWORD 113 ConvertActionToEvent(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. 134 void 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 235 void 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. 280 static 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 295 static DWORD 296 GetFilterFromEvents(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 } 304 305 // Restart a watch by using ReadDirectoryChangesW function 306 BOOL CDirectoryWatcher::RestartWatching() 307 { 308 assert(this != NULL); 309 310 if (IsDead()) 311 { 312 delete this; 313 return FALSE; // the watch is dead 314 } 315 316 // initialize the buffer and the overlapped 317 ZeroMemory(s_buffer, sizeof(s_buffer)); 318 ZeroMemory(&m_overlapped, sizeof(m_overlapped)); 319 m_overlapped.hEvent = (HANDLE)this; 320 321 // start the directory watch 322 DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS); 323 if (!ReadDirectoryChangesW(m_hDirectory, s_buffer, sizeof(s_buffer), 324 m_fRecursive, dwFilter, NULL, 325 &m_overlapped, _NotificationCompletion)) 326 { 327 ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n", 328 m_szDirectoryPath, GetLastError()); 329 return FALSE; // failure 330 } 331 332 return TRUE; // success 333 } 334 335 BOOL CDirectoryWatcher::CreateAPCThread() 336 { 337 if (s_hThreadAPC != NULL) 338 return TRUE; 339 340 unsigned tid; 341 s_fTerminateAllWatchers = FALSE; 342 s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirectoryWatcherThreadFuncAPC, 343 NULL, 0, &tid); 344 return s_hThreadAPC != NULL; 345 } 346 347 BOOL CDirectoryWatcher::RequestAddWatcher() 348 { 349 assert(this != NULL); 350 351 // create an APC thread for directory watching 352 if (!CreateAPCThread()) 353 return FALSE; 354 355 // request adding the watch 356 QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)this); 357 return TRUE; 358 } 359 360 BOOL CDirectoryWatcher::RequestTermination() 361 { 362 assert(this != NULL); 363 364 if (s_hThreadAPC) 365 { 366 QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)this); 367 return TRUE; 368 } 369 370 return FALSE; 371 } 372 373 /*static*/ void CDirectoryWatcher::RequestAllWatchersTermination() 374 { 375 if (!s_hThreadAPC) 376 return; 377 378 // request termination of all directory watches 379 QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL); 380 } 381 382 void CDirectoryWatcher::QuitWatching() 383 { 384 assert(this != NULL); 385 386 m_fDead = TRUE; 387 m_hNotifyWnd = NULL; 388 CancelIo(m_hDirectory); 389 } 390 391 BOOL CDirectoryWatcher::IsDead() 392 { 393 if (m_hNotifyWnd && !::IsWindow(m_hNotifyWnd)) 394 { 395 m_hNotifyWnd = NULL; 396 m_fDead = TRUE; 397 CancelIo(m_hDirectory); 398 } 399 return m_fDead; 400 } 401