Fix typos
[TortoiseGit.git] / src / TGitCache / DirectoryWatcher.cpp
blob4013e37aea594fc87d2e49b745d1368368a99185
1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2008, 2011-2012 - TortoiseSVN
4 // Copyright (C) 2008-2017, 2019-2023 - TortoiseGit
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "stdafx.h"
21 #include <Dbt.h>
22 #include "GitStatusCache.h"
23 #include "DirectoryWatcher.h"
24 #include "GitIndex.h"
25 #include "SmartHandle.h"
27 #include <list>
29 extern HWND hWndHidden;
30 extern CGitAdminDirMap g_AdminDirMap;
32 CDirectoryWatcher::CDirectoryWatcher()
34 // enable the required privileges for this process
36 LPCWSTR arPrivelegeNames[] = { SE_BACKUP_NAME,
37 SE_RESTORE_NAME,
38 SE_CHANGE_NOTIFY_NAME
41 for (int i = 0; i < (sizeof(arPrivelegeNames) / sizeof(LPCWSTR)); ++i)
43 CAutoGeneralHandle hToken;
44 if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, hToken.GetPointer()))
46 TOKEN_PRIVILEGES tp = { 1 };
48 if (LookupPrivilegeValue(nullptr, arPrivelegeNames[i], &tp.Privileges[0].Luid))
50 tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
52 AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr);
57 unsigned int threadId = 0;
58 m_hThread = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, ThreadEntry, this, 0, &threadId));
61 CDirectoryWatcher::~CDirectoryWatcher()
63 Stop();
64 AutoLocker lock(m_critSec);
65 ClearInfoMap();
66 CleanupWatchInfo();
69 void CDirectoryWatcher::CloseCompletionPort()
71 m_hCompPort.CloseHandle();
74 void CDirectoryWatcher::ScheduleForDeletion (CDirWatchInfo* info)
76 infoToDelete.push_back (info);
79 void CDirectoryWatcher::CleanupWatchInfo()
81 AutoLocker lock(m_critSec);
82 InterlockedExchange(&m_bCleaned, TRUE);
83 while (!infoToDelete.empty())
85 CDirWatchInfo* info = infoToDelete.back();
86 infoToDelete.pop_back();
87 delete info;
91 void CDirectoryWatcher::Stop()
93 InterlockedExchange(&m_bRunning, FALSE);
94 CloseWatchHandles();
95 WaitForSingleObject(m_hThread, 4000);
96 m_hThread.CloseHandle();
99 void CDirectoryWatcher::SetFolderCrawler(CFolderCrawler * crawler)
101 m_FolderCrawler = crawler;
104 bool CDirectoryWatcher::RemovePathAndChildren(const CTGitPath& path)
106 bool bRemoved = false;
107 AutoLocker lock(m_critSec);
108 repeat:
109 for (int i=0; i<watchedPaths.GetCount(); ++i)
111 if (path.IsAncestorOf(watchedPaths[i]))
113 watchedPaths.RemovePath(watchedPaths[i]);
114 bRemoved = true;
115 goto repeat;
118 return bRemoved;
121 void CDirectoryWatcher::BlockPath(const CTGitPath& path)
123 blockedPath = path;
124 // block the path from being watched for 4 seconds
125 blockTickCount = GetTickCount64() + 4000;
126 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Blocking path: %s\n", path.GetWinPath());
129 bool CDirectoryWatcher::AddPath(const CTGitPath& path, bool bCloseInfoMap)
131 if (!CGitStatusCache::Instance().IsPathAllowed(path))
132 return false;
133 if ((!blockedPath.IsEmpty())&&(blockedPath.IsAncestorOf(path)))
135 if (GetTickCount64() < blockTickCount)
137 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Path %s prevented from being watched\n", path.GetWinPath());
138 return false;
142 // ignore the recycle bin
143 PTSTR pFound = StrStrI(path.GetWinPath(), L":\\RECYCLER");
144 if (pFound)
146 if (*(pFound + wcslen(L":\\RECYCLER")) == L'\0' || *(pFound + wcslen(L":\\RECYCLER")) == L'\\')
147 return false;
149 pFound = StrStrI(path.GetWinPath(), L":\\$Recycle.Bin");
150 if (pFound)
152 if (*(pFound + wcslen(L":\\$Recycle.Bin")) == L'\0' || *(pFound + wcslen(L":\\$Recycle.Bin")) == L'\\')
153 return false;
156 AutoLocker lock(m_critSec);
157 for (int i=0; i<watchedPaths.GetCount(); ++i)
159 if (watchedPaths[i].IsAncestorOf(path))
160 return false; // already watched (recursively)
163 // now check if with the new path we might have a new root
164 CTGitPath newroot;
165 for (int i=0; i<watchedPaths.GetCount(); ++i)
167 const CString& watched = watchedPaths[i].GetWinPathString();
168 const CString& sPath = path.GetWinPathString();
169 const int minlen = min(sPath.GetLength(), watched.GetLength());
170 int len = 0;
171 for (len = 0; len < minlen; ++len)
173 if (watched.GetAt(len) != sPath.GetAt(len))
175 if ((len > 1)&&(len < minlen))
177 if (sPath.GetAt(len)=='\\')
178 newroot = CTGitPath(sPath.Left(len));
179 else if (watched.GetAt(len)=='\\')
180 newroot = CTGitPath(watched.Left(len));
182 break;
185 if (len == minlen)
187 if (sPath.GetLength() == minlen)
189 if (watched.GetLength() > minlen)
191 if (watched.GetAt(len)=='\\')
192 newroot = path;
193 else if (sPath.GetLength() == 3 && sPath[1] == ':')
194 newroot = path;
197 else
199 if (sPath.GetLength() > minlen)
201 if (sPath.GetAt(len)=='\\')
202 newroot = CTGitPath(watched);
203 else if (watched.GetLength() == 3 && watched[1] == ':')
204 newroot = CTGitPath(watched);
209 if (!newroot.IsEmpty() && newroot.HasAdminDir())
211 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": add path to watch %s\n", newroot.GetWinPath());
212 watchedPaths.AddPath(newroot);
213 watchedPaths.RemoveChildren();
214 if (bCloseInfoMap)
215 ClearInfoMap();
217 return true;
219 if (!path.HasAdminDir())
221 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": Path %s prevented from being watched: not versioned\n", path.GetWinPath());
222 return false;
224 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": add path to watch %s\n", path.GetWinPath());
225 watchedPaths.AddPath(path);
226 if (bCloseInfoMap)
227 ClearInfoMap();
229 return true;
232 bool CDirectoryWatcher::IsPathWatched(const CTGitPath& path)
234 AutoLocker lock(m_critSec);
235 for (int i=0; i<watchedPaths.GetCount(); ++i)
237 if (watchedPaths[i].IsAncestorOf(path))
238 return true;
240 return false;
243 unsigned int CDirectoryWatcher::ThreadEntry(void* pContext)
245 reinterpret_cast<CDirectoryWatcher*>(pContext)->WorkerThread();
246 return 0;
249 void CDirectoryWatcher::WorkerThread()
251 DWORD numBytes;
252 CDirWatchInfo* pdi = nullptr;
253 LPOVERLAPPED lpOverlapped;
254 WCHAR buf[READ_DIR_CHANGE_BUFFER_SIZE] = {0};
255 WCHAR* pFound = nullptr;
256 while (m_bRunning)
258 CleanupWatchInfo();
259 if (!watchedPaths.IsEmpty())
261 // Any incoming notifications?
263 pdi = nullptr;
264 numBytes = 0;
265 InterlockedExchange(&m_bCleaned, FALSE);
266 if ((!m_hCompPort)
267 || !GetQueuedCompletionStatus(m_hCompPort,
268 &numBytes,
269 (PULONG_PTR) &pdi,
270 &lpOverlapped,
271 600000 /*10 minutes*/))
273 // No. Still trying?
275 if (!m_bRunning)
276 return;
278 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": restarting watcher\n");
279 m_hCompPort.CloseHandle();
281 // We must sync the whole section because other threads may
282 // receive "AddPath" calls that will delete the completion
283 // port *while* we are adding references to it .
285 AutoLocker lock(m_critSec);
287 // Clear the list of watched objects and recreate that list.
288 // This will also delete the old completion port
290 ClearInfoMap();
291 CleanupWatchInfo();
293 for (int i=0; i<watchedPaths.GetCount(); ++i)
295 CTGitPath watchedPath = watchedPaths[i];
297 CAutoFile hDir = CreateFile(watchedPath.GetWinPath(),
298 FILE_LIST_DIRECTORY,
299 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
300 nullptr, //security attributes
301 OPEN_EXISTING,
302 FILE_FLAG_BACKUP_SEMANTICS | //required privileges: SE_BACKUP_NAME and SE_RESTORE_NAME.
303 FILE_FLAG_OVERLAPPED,
304 nullptr);
305 if (!hDir)
307 // this could happen if a watched folder has been removed/renamed
308 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": CreateFile failed. Can't watch directory %s\n", watchedPaths[i].GetWinPath());
309 watchedPaths.RemovePath(watchedPath);
310 break;
313 DEV_BROADCAST_HANDLE NotificationFilter = { 0 };
314 NotificationFilter.dbch_size = sizeof(DEV_BROADCAST_HANDLE);
315 NotificationFilter.dbch_devicetype = DBT_DEVTYP_HANDLE;
316 NotificationFilter.dbch_handle = hDir;
317 // RegisterDeviceNotification sends a message to the UI thread:
318 // make sure we *can* send it and that the UI thread isn't waiting on a lock
319 const int numPaths = watchedPaths.GetCount();
320 const size_t numWatch = watchInfoMap.size();
321 lock.Unlock();
322 NotificationFilter.dbch_hdevnotify = RegisterDeviceNotification(hWndHidden, &NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
323 lock.Lock();
324 // since we released the lock to prevent a deadlock with the UI thread,
325 // it could happen that new paths were added to watch, or another thread
326 // could have cleared our info map.
327 // if that happened, we have to restart watching all paths again.
328 if ((numPaths != watchedPaths.GetCount()) || (numWatch != watchInfoMap.size()))
330 ClearInfoMap();
331 CleanupWatchInfo();
332 Sleep(200);
333 break;
336 CDirWatchInfo * pDirInfo = new CDirWatchInfo(hDir.Detach(), watchedPath);// the new CDirWatchInfo object owns the handle now
337 pDirInfo->m_hDevNotify = NotificationFilter.dbch_hdevnotify;
340 HANDLE port = CreateIoCompletionPort(pDirInfo->m_hDir, m_hCompPort, reinterpret_cast<ULONG_PTR>(pDirInfo), 0);
341 if (port == INVALID_HANDLE_VALUE)
343 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": CreateIoCompletionPort failed. Can't watch directory %s\n", watchedPath.GetWinPath());
345 // we must close the directory handle to allow ClearInfoMap()
346 // to close the completion port properly
347 pDirInfo->CloseDirectoryHandle();
349 ClearInfoMap();
350 CleanupWatchInfo();
351 delete pDirInfo;
352 pDirInfo = nullptr;
354 watchedPaths.RemovePath(watchedPath);
355 break;
357 m_hCompPort = std::move(port);
359 if (!ReadDirectoryChangesW(pDirInfo->m_hDir,
360 pDirInfo->m_Buffer,
361 READ_DIR_CHANGE_BUFFER_SIZE,
362 TRUE,
363 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
364 &numBytes,// not used
365 &pDirInfo->m_Overlapped,
366 nullptr)) //no completion routine!
368 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": ReadDirectoryChangesW failed. Can't watch directory %s\n", watchedPath.GetWinPath());
370 // we must close the directory handle to allow ClearInfoMap()
371 // to close the completion port properly
372 pDirInfo->CloseDirectoryHandle();
374 ClearInfoMap();
375 CleanupWatchInfo();
376 delete pDirInfo;
377 pDirInfo = nullptr;
378 watchedPaths.RemovePath(watchedPath);
379 break;
382 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": watching path %s\n", pDirInfo->m_DirName.GetWinPath());
383 watchInfoMap[pDirInfo->m_hDir] = pDirInfo;
386 else
388 if (!m_bRunning)
389 return;
390 if (watchInfoMap.empty())
391 continue;
393 // NOTE: the longer this code takes to execute until ReadDirectoryChangesW
394 // is called again, the higher the chance that we miss some
395 // changes in the file system!
396 if (pdi)
398 BOOL bRet = false;
399 std::list<CTGitPath> notifyPaths;
401 AutoLocker lock(m_critSec);
402 // in case the CDirectoryWatcher objects have been cleaned,
403 // the m_bCleaned variable will be set to true here. If the
404 // objects haven't been cleared, we can access them here.
405 if (InterlockedExchange(&m_bCleaned, FALSE))
406 continue;
407 if ( (!pdi->m_hDir) || watchInfoMap.empty()
408 || (watchInfoMap.find(pdi->m_hDir) == watchInfoMap.end()))
410 continue;
412 auto pnotify = reinterpret_cast<PFILE_NOTIFY_INFORMATION>(pdi->m_Buffer);
413 DWORD nOffset = 0;
417 pnotify = reinterpret_cast<PFILE_NOTIFY_INFORMATION>(reinterpret_cast<LPBYTE>(pnotify) + nOffset);
419 if (reinterpret_cast<ULONG_PTR>(pnotify) - reinterpret_cast<ULONG_PTR>(pdi->m_Buffer) > READ_DIR_CHANGE_BUFFER_SIZE)
420 break;
422 nOffset = pnotify->NextEntryOffset;
424 if (pnotify->FileNameLength >= (READ_DIR_CHANGE_BUFFER_SIZE * sizeof(wchar_t)))
425 continue;
427 SecureZeroMemory(buf, READ_DIR_CHANGE_BUFFER_SIZE * sizeof(wchar_t));
428 wcsncpy_s(buf, pdi->m_DirPath, _countof(buf) - 1);
429 errno_t err = wcsncat_s(buf + pdi->m_DirPath.GetLength(), READ_DIR_CHANGE_BUFFER_SIZE - pdi->m_DirPath.GetLength(), pnotify->FileName, min(READ_DIR_CHANGE_BUFFER_SIZE - pdi->m_DirPath.GetLength(), int(pnotify->FileNameLength / sizeof(wchar_t))));
430 if (err == STRUNCATE)
431 continue;
432 buf[(pnotify->FileNameLength / sizeof(wchar_t)) + pdi->m_DirPath.GetLength()] = L'\0';
434 if (m_FolderCrawler)
436 if ((pFound = StrStrI(buf, L"\\tmp")) != nullptr)
438 pFound += wcslen(L"\\tmp");
439 if (*pFound == L'\\' || *pFound == L'\0')
440 continue;
442 if ((pFound = StrStrI(buf, L":\\RECYCLER")) != nullptr)
444 if (*(pFound + wcslen(L":\\RECYCLER")) == L'\0' || *(pFound + wcslen(L":\\RECYCLER")) == L'\\')
445 continue;
447 if ((pFound = StrStrI(buf, L":\\$Recycle.Bin")) != nullptr)
449 if (*(pFound + wcslen(L":\\$Recycle.Bin")) == L'\0' || *(pFound + wcslen(L":\\$Recycle.Bin")) == L'\\')
450 continue;
453 if (StrStrI(buf, L".tmp"))
455 // assume files with a .tmp extension are not versioned and interesting,
456 // so ignore them.
457 continue;
460 CTGitPath path;
461 bool isIndex = false;
462 if ((pFound = wcsstr(buf, L".git")) != nullptr)
464 // omit repository data change except .git/index.lock- or .git/HEAD.lock-files
465 if (reinterpret_cast<ULONG_PTR>(pnotify) - reinterpret_cast<ULONG_PTR>(pdi->m_Buffer) > READ_DIR_CHANGE_BUFFER_SIZE)
466 break;
468 path = g_AdminDirMap.GetWorkingCopy(CTGitPath(buf).GetContainingDirectory().GetWinPathString());
470 if ((wcsstr(pFound, L"index.lock") || wcsstr(pFound, L"HEAD.lock")) && pnotify->Action == FILE_ACTION_ADDED)
472 CGitStatusCache::Instance().BlockPath(path);
473 continue;
475 else if (((wcsstr(pFound, L"index.lock") || wcsstr(pFound, L"HEAD.lock")) && pnotify->Action == FILE_ACTION_REMOVED) || (((wcsstr(pFound, L"index") && !wcsstr(pFound, L"index.lock")) || (wcsstr(pFound, L"HEAD") && wcsstr(pFound, L"HEAD.lock"))) && pnotify->Action == FILE_ACTION_MODIFIED) || ((!wcsstr(pFound, L"index.lock") || wcsstr(pFound, L"HEAD.lock")) && pnotify->Action == FILE_ACTION_RENAMED_NEW_NAME))
477 isIndex = true;
478 CGitStatusCache::Instance().BlockPath(path, 1);
480 else
481 continue;
483 else
484 path.SetFromUnknown(buf);
486 if(!path.HasAdminDir() && !isIndex)
487 continue;
489 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) L": change notification for %s\n", buf);
490 notifyPaths.push_back(path);
492 } while ((nOffset > 0)&&(nOffset < READ_DIR_CHANGE_BUFFER_SIZE));
494 // setup next notification cycle
495 SecureZeroMemory (pdi->m_Buffer, sizeof(pdi->m_Buffer));
496 SecureZeroMemory (&pdi->m_Overlapped, sizeof(OVERLAPPED));
497 bRet = ReadDirectoryChangesW(pdi->m_hDir,
498 pdi->m_Buffer,
499 READ_DIR_CHANGE_BUFFER_SIZE,
500 TRUE,
501 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
502 &numBytes,// not used
503 &pdi->m_Overlapped,
504 nullptr); //no completion routine!
506 if (!notifyPaths.empty())
508 for (auto nit = notifyPaths.cbegin(); nit != notifyPaths.cend(); ++nit)
510 m_FolderCrawler->AddPathForUpdate(*nit);
514 // any clean-up to do?
516 CleanupWatchInfo();
518 if (!bRet)
520 // Since the call to ReadDirectoryChangesW failed, just
521 // wait a while. We don't want to have this thread
522 // running using 100% CPU if something goes completely
523 // wrong.
524 pdi->CloseDirectoryHandle();
525 watchedPaths.RemovePath(pdi->m_DirName);
526 Sleep(200);
527 CloseCompletionPort();
531 }// if (!watchedPaths.IsEmpty())
532 else
533 Sleep(200);
534 }// while (m_bRunning)
537 // call this before destroying async I/O structures:
539 void CDirectoryWatcher::CloseWatchHandles()
541 AutoLocker lock(m_critSec);
543 for (auto I = watchInfoMap.cbegin(); I != watchInfoMap.cend(); ++I)
544 if (I->second)
545 I->second->CloseDirectoryHandle();
547 CloseCompletionPort();
550 void CDirectoryWatcher::ClearInfoMap()
552 CloseWatchHandles();
553 if (!watchInfoMap.empty())
555 AutoLocker lock(m_critSec);
556 for (TInfoMap::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
558 CDirectoryWatcher::CDirWatchInfo * info = I->second;
559 I->second = nullptr;
560 ScheduleForDeletion (info);
562 watchInfoMap.clear();
566 CTGitPath CDirectoryWatcher::CloseInfoMap(HANDLE hDir)
568 CTGitPath path;
569 AutoLocker lock(m_critSec);
570 TInfoMap::const_iterator d = watchInfoMap.find(hDir);
571 if (d != watchInfoMap.end())
573 path = CTGitPath(CTGitPath(d->second->m_DirPath).GetRootPathString());
574 RemovePathAndChildren(path);
575 BlockPath(path);
577 CloseWatchHandles();
579 if (watchInfoMap.empty())
580 return path;
582 for (auto I = watchInfoMap.cbegin(); I != watchInfoMap.cend(); ++I)
584 CDirectoryWatcher::CDirWatchInfo * info = I->second;
586 ScheduleForDeletion (info);
588 watchInfoMap.clear();
590 return path;
593 bool CDirectoryWatcher::CloseHandlesForPath(const CTGitPath& path)
595 AutoLocker lock(m_critSec);
596 CloseWatchHandles();
598 if (watchInfoMap.empty())
599 return false;
601 for (TInfoMap::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
603 CDirectoryWatcher::CDirWatchInfo * info = I->second;
604 I->second = nullptr;
605 CTGitPath p = CTGitPath(info->m_DirPath);
606 if (path.IsAncestorOf(p))
608 RemovePathAndChildren(p);
609 BlockPath(p);
611 ScheduleForDeletion(info);
613 watchInfoMap.clear();
614 return true;
617 CDirectoryWatcher::CDirWatchInfo::CDirWatchInfo(HANDLE hDir, const CTGitPath& DirectoryName)
618 : m_hDir(std::move(hDir))
619 , m_DirName(DirectoryName)
621 ATLASSERT(m_hDir && !DirectoryName.IsEmpty());
622 m_Buffer[0] = '\0';
623 m_DirPath = m_DirName.GetWinPathString();
624 if (m_DirPath.GetAt(m_DirPath.GetLength() - 1) != L'\\')
625 m_DirPath += L'\\';
628 CDirectoryWatcher::CDirWatchInfo::~CDirWatchInfo()
630 CloseDirectoryHandle();
633 bool CDirectoryWatcher::CDirWatchInfo::CloseDirectoryHandle()
635 bool b = m_hDir.CloseHandle();
637 if (m_hDevNotify)
639 UnregisterDeviceNotification(m_hDevNotify);
640 m_hDevNotify = nullptr;
642 return b;