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(LPCWSTR pszDirectoryPath, BOOL fSubTree) 72 : m_fDead(FALSE) 73 , m_fRecursive(fSubTree) 74 , m_dir_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 TRACE("CDirectoryWatcher::ProcessNotification: enter\n"); 142 143 // for each entry in s_buffer 144 szPath[0] = szTempPath[0] = 0; 145 for (;;) 146 { 147 // get name (relative from m_szDirectoryPath) 148 cbName = pInfo->FileNameLength; 149 if (sizeof(szName) - sizeof(UNICODE_NULL) < cbName) 150 { 151 ERR("pInfo->FileName is longer than szName\n"); 152 break; 153 } 154 // NOTE: FILE_NOTIFY_INFORMATION.FileName is not null-terminated. 155 ZeroMemory(szName, sizeof(szName)); 156 CopyMemory(szName, pInfo->FileName, cbName); 157 158 // get full path 159 lstrcpynW(szPath, m_szDirectoryPath, _countof(szPath)); 160 PathAppendW(szPath, szName); 161 162 // convert to long pathname if it contains '~' 163 if (StrChrW(szPath, L'~') != NULL) 164 { 165 GetLongPathNameW(szPath, szName, _countof(szName)); 166 lstrcpynW(szPath, szName, _countof(szPath)); 167 } 168 169 // convert action to event 170 fDir = PathIsDirectoryW(szPath); 171 dwEvent = ConvertActionToEvent(pInfo->Action, fDir); 172 173 // convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory 174 if (!fDir && (dwEvent == SHCNE_DELETE) && m_dir_list.ContainsPath(szPath)) 175 { 176 fDir = TRUE; 177 dwEvent = SHCNE_RMDIR; 178 } 179 180 // update m_dir_list 181 switch (dwEvent) 182 { 183 case SHCNE_MKDIR: 184 if (!PathIsDirectoryW(szPath) || !m_dir_list.AddPath(szPath)) 185 dwEvent = 0; 186 break; 187 case SHCNE_CREATE: 188 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath)) 189 dwEvent = 0; 190 break; 191 case SHCNE_RENAMEFOLDER: 192 if (!PathIsDirectoryW(szPath) || !m_dir_list.RenamePath(szTempPath, szPath)) 193 dwEvent = 0; 194 break; 195 case SHCNE_RENAMEITEM: 196 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath)) 197 dwEvent = 0; 198 break; 199 case SHCNE_RMDIR: 200 if (PathIsDirectoryW(szPath) || !m_dir_list.DeletePath(szPath)) 201 dwEvent = 0; 202 break; 203 case SHCNE_DELETE: 204 if (PathFileExistsW(szPath)) 205 dwEvent = 0; 206 break; 207 } 208 209 if (dwEvent != 0) 210 { 211 // notify 212 if (pInfo->Action == FILE_ACTION_RENAMED_NEW_NAME) 213 NotifyFileSystemChange(dwEvent, szTempPath, szPath); 214 else 215 NotifyFileSystemChange(dwEvent, szPath, NULL); 216 } 217 else if (pInfo->Action == FILE_ACTION_RENAMED_OLD_NAME) 218 { 219 // save path for next FILE_ACTION_RENAMED_NEW_NAME 220 lstrcpynW(szTempPath, szPath, MAX_PATH); 221 } 222 223 if (pInfo->NextEntryOffset == 0) 224 break; // there is no next entry 225 226 // go next entry 227 pInfo = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pInfo + pInfo->NextEntryOffset); 228 } 229 230 TRACE("CDirectoryWatcher::ProcessNotification: leave\n"); 231 } 232 233 void CDirectoryWatcher::ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered) 234 { 235 // If the FSD doesn't support directory change notifications, there's no 236 // no need to retry and requeue notification 237 if (dwErrorCode == ERROR_INVALID_FUNCTION) 238 { 239 ERR("ERROR_INVALID_FUNCTION\n"); 240 return; 241 } 242 243 // Also, if the notify operation was canceled (like, user moved to another 244 // directory), then, don't requeue notification. 245 if (dwErrorCode == ERROR_OPERATION_ABORTED) 246 { 247 TRACE("ERROR_OPERATION_ABORTED\n"); 248 if (IsDead()) 249 delete this; 250 return; 251 } 252 253 // is this watch dead? 254 if (IsDead()) 255 { 256 TRACE("IsDead()\n"); 257 delete this; 258 return; 259 } 260 261 // This likely means overflow, so force whole directory refresh. 262 if (dwNumberOfBytesTransfered == 0) 263 { 264 // do notify a SHCNE_UPDATEDIR 265 NotifyFileSystemChange(SHCNE_UPDATEDIR, m_szDirectoryPath, NULL); 266 } 267 else 268 { 269 // do notify 270 ProcessNotification(); 271 } 272 273 // restart a watch 274 RestartWatching(); 275 } 276 277 // The completion routine of ReadDirectoryChangesW. 278 static void CALLBACK 279 _NotificationCompletion(DWORD dwErrorCode, 280 DWORD dwNumberOfBytesTransfered, 281 LPOVERLAPPED lpOverlapped) 282 { 283 // MSDN: The hEvent member of the OVERLAPPED structure is not used by the 284 // system in this case, so you can use it yourself. We do just this, storing 285 // a pointer to the working struct in the overlapped structure. 286 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)lpOverlapped->hEvent; 287 assert(pDirectoryWatcher != NULL); 288 289 pDirectoryWatcher->ReadCompletion(dwErrorCode, dwNumberOfBytesTransfered); 290 } 291 292 // convert events to notification filter 293 static DWORD 294 GetFilterFromEvents(DWORD fEvents) 295 { 296 // FIXME 297 return (FILE_NOTIFY_CHANGE_FILE_NAME | 298 FILE_NOTIFY_CHANGE_DIR_NAME | 299 FILE_NOTIFY_CHANGE_CREATION | 300 FILE_NOTIFY_CHANGE_SIZE); 301 } 302 303 // Restart a watch by using ReadDirectoryChangesW function 304 BOOL CDirectoryWatcher::RestartWatching() 305 { 306 assert(this != NULL); 307 308 if (IsDead()) 309 { 310 delete this; 311 return FALSE; // the watch is dead 312 } 313 314 // initialize the buffer and the overlapped 315 ZeroMemory(s_buffer, sizeof(s_buffer)); 316 ZeroMemory(&m_overlapped, sizeof(m_overlapped)); 317 m_overlapped.hEvent = (HANDLE)this; 318 319 // start the directory watch 320 DWORD dwFilter = GetFilterFromEvents(SHCNE_ALLEVENTS); 321 if (!ReadDirectoryChangesW(m_hDirectory, s_buffer, sizeof(s_buffer), 322 m_fRecursive, dwFilter, NULL, 323 &m_overlapped, _NotificationCompletion)) 324 { 325 ERR("ReadDirectoryChangesW for '%S' failed (error: %ld)\n", 326 m_szDirectoryPath, GetLastError()); 327 return FALSE; // failure 328 } 329 330 return TRUE; // success 331 } 332 333 BOOL CDirectoryWatcher::CreateAPCThread() 334 { 335 if (s_hThreadAPC != NULL) 336 return TRUE; 337 338 unsigned tid; 339 s_fTerminateAllWatchers = FALSE; 340 s_hThreadAPC = (HANDLE)_beginthreadex(NULL, 0, DirectoryWatcherThreadFuncAPC, 341 NULL, 0, &tid); 342 return s_hThreadAPC != NULL; 343 } 344 345 BOOL CDirectoryWatcher::RequestAddWatcher() 346 { 347 assert(this != NULL); 348 349 // create an APC thread for directory watching 350 if (!CreateAPCThread()) 351 return FALSE; 352 353 // request adding the watch 354 QueueUserAPC(_AddDirectoryProcAPC, s_hThreadAPC, (ULONG_PTR)this); 355 return TRUE; 356 } 357 358 BOOL CDirectoryWatcher::RequestTermination() 359 { 360 assert(this != NULL); 361 362 if (s_hThreadAPC) 363 { 364 QueueUserAPC(_RequestTerminationAPC, s_hThreadAPC, (ULONG_PTR)this); 365 return TRUE; 366 } 367 368 return FALSE; 369 } 370 371 /*static*/ void CDirectoryWatcher::RequestAllWatchersTermination() 372 { 373 if (!s_hThreadAPC) 374 return; 375 376 // request termination of all directory watches 377 QueueUserAPC(_RequestAllTerminationAPC, s_hThreadAPC, (ULONG_PTR)NULL); 378 } 379 380 void CDirectoryWatcher::QuitWatching() 381 { 382 assert(this != NULL); 383 384 m_fDead = TRUE; 385 CancelIo(m_hDirectory); 386 } 387 388 BOOL CDirectoryWatcher::IsDead() const 389 { 390 return m_fDead; 391 } 392