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 if (GetLongPathNameW(szPath, szName, _countof(szName)) && 166 !PathIsRelativeW(szName)) 167 { 168 lstrcpynW(szPath, szName, _countof(szPath)); 169 } 170 } 171 172 // convert action to event 173 fDir = PathIsDirectoryW(szPath); 174 dwEvent = ConvertActionToEvent(pInfo->Action, fDir); 175 176 // convert SHCNE_DELETE to SHCNE_RMDIR if the path is a directory 177 if (!fDir && (dwEvent == SHCNE_DELETE) && m_dir_list.ContainsPath(szPath)) 178 { 179 fDir = TRUE; 180 dwEvent = SHCNE_RMDIR; 181 } 182 183 // update m_dir_list 184 switch (dwEvent) 185 { 186 case SHCNE_MKDIR: 187 if (!PathIsDirectoryW(szPath) || !m_dir_list.AddPath(szPath)) 188 dwEvent = 0; 189 break; 190 case SHCNE_CREATE: 191 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath)) 192 dwEvent = 0; 193 break; 194 case SHCNE_RENAMEFOLDER: 195 if (!PathIsDirectoryW(szPath) || !m_dir_list.RenamePath(szTempPath, szPath)) 196 dwEvent = 0; 197 break; 198 case SHCNE_RENAMEITEM: 199 if (!PathFileExistsW(szPath) || PathIsDirectoryW(szPath)) 200 dwEvent = 0; 201 break; 202 case SHCNE_RMDIR: 203 if (PathIsDirectoryW(szPath) || !m_dir_list.DeletePath(szPath)) 204 dwEvent = 0; 205 break; 206 case SHCNE_DELETE: 207 if (PathFileExistsW(szPath)) 208 dwEvent = 0; 209 break; 210 } 211 212 if (dwEvent != 0) 213 { 214 // notify 215 if (pInfo->Action == FILE_ACTION_RENAMED_NEW_NAME) 216 NotifyFileSystemChange(dwEvent, szTempPath, szPath); 217 else 218 NotifyFileSystemChange(dwEvent, szPath, NULL); 219 } 220 else if (pInfo->Action == FILE_ACTION_RENAMED_OLD_NAME) 221 { 222 // save path for next FILE_ACTION_RENAMED_NEW_NAME 223 lstrcpynW(szTempPath, szPath, MAX_PATH); 224 } 225 226 if (pInfo->NextEntryOffset == 0) 227 break; // there is no next entry 228 229 // go next entry 230 pInfo = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pInfo + pInfo->NextEntryOffset); 231 } 232 233 TRACE("CDirectoryWatcher::ProcessNotification: leave\n"); 234 } 235 236 void CDirectoryWatcher::ReadCompletion(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered) 237 { 238 // If the FSD doesn't support directory change notifications, there's no 239 // no need to retry and requeue notification 240 if (dwErrorCode == ERROR_INVALID_FUNCTION) 241 { 242 ERR("ERROR_INVALID_FUNCTION\n"); 243 return; 244 } 245 246 // Also, if the notify operation was canceled (like, user moved to another 247 // directory), then, don't requeue notification. 248 if (dwErrorCode == ERROR_OPERATION_ABORTED) 249 { 250 TRACE("ERROR_OPERATION_ABORTED\n"); 251 if (IsDead()) 252 delete this; 253 return; 254 } 255 256 // is this watch dead? 257 if (IsDead()) 258 { 259 TRACE("IsDead()\n"); 260 delete this; 261 return; 262 } 263 264 // This likely means overflow, so force whole directory refresh. 265 if (dwNumberOfBytesTransfered == 0) 266 { 267 // do notify a SHCNE_UPDATEDIR 268 NotifyFileSystemChange(SHCNE_UPDATEDIR, m_szDirectoryPath, NULL); 269 } 270 else 271 { 272 // do notify 273 ProcessNotification(); 274 } 275 276 // restart a watch 277 RestartWatching(); 278 } 279 280 // The completion routine of ReadDirectoryChangesW. 281 static void CALLBACK 282 _NotificationCompletion(DWORD dwErrorCode, 283 DWORD dwNumberOfBytesTransfered, 284 LPOVERLAPPED lpOverlapped) 285 { 286 // MSDN: The hEvent member of the OVERLAPPED structure is not used by the 287 // system in this case, so you can use it yourself. We do just this, storing 288 // a pointer to the working struct in the overlapped structure. 289 CDirectoryWatcher *pDirectoryWatcher = (CDirectoryWatcher *)lpOverlapped->hEvent; 290 assert(pDirectoryWatcher != NULL); 291 292 pDirectoryWatcher->ReadCompletion(dwErrorCode, dwNumberOfBytesTransfered); 293 } 294 295 // convert events to notification filter 296 static DWORD 297 GetFilterFromEvents(DWORD fEvents) 298 { 299 // FIXME 300 return (FILE_NOTIFY_CHANGE_FILE_NAME | 301 FILE_NOTIFY_CHANGE_DIR_NAME | 302 FILE_NOTIFY_CHANGE_CREATION | 303 FILE_NOTIFY_CHANGE_SIZE); 304 } 305 306 // Restart a watch by using ReadDirectoryChangesW function 307 BOOL 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 336 BOOL 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 348 BOOL 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 361 BOOL 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 383 void CDirectoryWatcher::QuitWatching() 384 { 385 assert(this != NULL); 386 387 m_fDead = TRUE; 388 CancelIo(m_hDirectory); 389 } 390 391 BOOL CDirectoryWatcher::IsDead() const 392 { 393 return m_fDead; 394 } 395