optimize TGitCache for CLI operations
[TortoiseGit.git] / src / TGitCache / DirectoryWatcher.cpp
blob0725d1e7696e7acea5b9e4bda2f79a973333eb43
1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005 - 2007 - TortoiseSVN
4 // Copyright (C) 2008-2011 - 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"
25 extern HWND hWnd;
27 CDirectoryWatcher::CDirectoryWatcher(void) : m_hCompPort(NULL)
28 , m_bRunning(TRUE)
29 , m_FolderCrawler(NULL)
30 , blockTickCount(0)
32 // enable the required privileges for this process
34 LPCTSTR arPrivelegeNames[] = { SE_BACKUP_NAME,
35 SE_RESTORE_NAME,
36 SE_CHANGE_NOTIFY_NAME
39 for (int i=0; i<(sizeof(arPrivelegeNames)/sizeof(LPCTSTR)); ++i)
41 HANDLE hToken;
42 if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
44 TOKEN_PRIVILEGES tp = { 1 };
46 if (LookupPrivilegeValue(NULL, arPrivelegeNames[i], &tp.Privileges[0].Luid))
48 tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
50 AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
52 CloseHandle(hToken);
56 unsigned int threadId;
57 m_hThread = (HANDLE)_beginthreadex(NULL,0,ThreadEntry,this,0,&threadId);
60 CDirectoryWatcher::~CDirectoryWatcher(void)
62 InterlockedExchange(&m_bRunning, FALSE);
63 if (m_hThread != INVALID_HANDLE_VALUE)
65 CloseHandle(m_hThread);
66 m_hThread = INVALID_HANDLE_VALUE;
68 AutoLocker lock(m_critSec);
69 ClearInfoMap();
72 void CDirectoryWatcher::Stop()
74 InterlockedExchange(&m_bRunning, FALSE);
75 if (m_hThread != INVALID_HANDLE_VALUE)
76 CloseHandle(m_hThread);
77 m_hThread = INVALID_HANDLE_VALUE;
78 if (m_hCompPort != INVALID_HANDLE_VALUE)
79 CloseHandle(m_hCompPort);
80 m_hCompPort = INVALID_HANDLE_VALUE;
83 void CDirectoryWatcher::SetFolderCrawler(CFolderCrawler * crawler)
85 m_FolderCrawler = crawler;
88 bool CDirectoryWatcher::RemovePathAndChildren(const CTGitPath& path)
90 bool bRemoved = false;
91 AutoLocker lock(m_critSec);
92 repeat:
93 for (int i=0; i<watchedPaths.GetCount(); ++i)
95 if (path.IsAncestorOf(watchedPaths[i]))
97 watchedPaths.RemovePath(watchedPaths[i]);
98 bRemoved = true;
99 goto repeat;
102 return bRemoved;
105 void CDirectoryWatcher::BlockPath(const CTGitPath& path)
107 blockedPath = path;
108 // block the path from being watched for 4 seconds
109 blockTickCount = GetTickCount()+4000;
110 ATLTRACE(_T("Blocking path: %s\n"), path.GetWinPath());
113 bool CDirectoryWatcher::AddPath(const CTGitPath& path)
115 if (!CGitStatusCache::Instance().IsPathAllowed(path))
116 return false;
117 if ((!blockedPath.IsEmpty())&&(blockedPath.IsAncestorOf(path)))
119 if (GetTickCount() < blockTickCount)
121 ATLTRACE(_T("Path %s prevented from being watched\n"), path.GetWinPath());
122 return false;
125 AutoLocker lock(m_critSec);
126 for (int i=0; i<watchedPaths.GetCount(); ++i)
128 if (watchedPaths[i].IsAncestorOf(path))
129 return false; // already watched (recursively)
132 // now check if with the new path we might have a new root
133 CTGitPath newroot;
134 for (int i=0; i<watchedPaths.GetCount(); ++i)
136 const CString& watched = watchedPaths[i].GetWinPathString();
137 const CString& sPath = path.GetWinPathString();
138 int minlen = min(sPath.GetLength(), watched.GetLength());
139 int len = 0;
140 for (len = 0; len < minlen; ++len)
142 if (watched.GetAt(len) != sPath.GetAt(len))
144 if ((len > 1)&&(len < minlen))
146 if (sPath.GetAt(len)=='\\')
148 newroot = CTGitPath(sPath.Left(len));
150 else if (watched.GetAt(len)=='\\')
152 newroot = CTGitPath(watched.Left(len));
155 break;
158 if (len == minlen)
160 if (sPath.GetLength() == minlen)
162 if (watched.GetLength() > minlen)
164 if (watched.GetAt(len)=='\\')
166 newroot = path;
168 else if (sPath.GetLength() == 3 && sPath[1] == ':')
170 newroot = path;
174 else
176 if (sPath.GetLength() > minlen)
178 if (sPath.GetAt(len)=='\\')
180 newroot = CTGitPath(watched);
182 else if (watched.GetLength() == 3 && watched[1] == ':')
184 newroot = CTGitPath(watched);
190 if (!newroot.IsEmpty())
192 ATLTRACE(_T("add path to watch %s\n"), newroot.GetWinPath());
193 watchedPaths.AddPath(newroot);
194 watchedPaths.RemoveChildren();
195 CloseInfoMap();
196 m_hCompPort = INVALID_HANDLE_VALUE;
197 return true;
199 ATLTRACE(_T("add path to watch %s\n"), path.GetWinPath());
200 watchedPaths.AddPath(path);
201 CloseInfoMap();
202 m_hCompPort = INVALID_HANDLE_VALUE;
203 return true;
206 bool CDirectoryWatcher::IsPathWatched(const CTGitPath& path)
208 AutoLocker lock(m_critSec);
209 for (int i=0; i<watchedPaths.GetCount(); ++i)
211 if (watchedPaths[i].IsAncestorOf(path))
212 return true;
214 return false;
217 unsigned int CDirectoryWatcher::ThreadEntry(void* pContext)
219 ((CDirectoryWatcher*)pContext)->WorkerThread();
220 return 0;
223 void CDirectoryWatcher::WorkerThread()
225 DWORD numBytes;
226 CDirWatchInfo * pdi = NULL;
227 LPOVERLAPPED lpOverlapped;
228 WCHAR buf[READ_DIR_CHANGE_BUFFER_SIZE] = {0};
229 WCHAR * pFound = NULL;
230 CTGitPath path;
232 while (m_bRunning)
234 if (watchedPaths.GetCount())
236 if (!GetQueuedCompletionStatus(m_hCompPort,
237 &numBytes,
238 (PULONG_PTR) &pdi,
239 &lpOverlapped,
240 INFINITE))
242 // Error retrieving changes
243 // Clear the list of watched objects and recreate that list
244 if (!m_bRunning)
245 return;
247 AutoLocker lock(m_critSec);
248 ClearInfoMap();
250 DWORD lasterr = GetLastError();
251 if ((m_hCompPort != INVALID_HANDLE_VALUE)&&(lasterr!=ERROR_SUCCESS)&&(lasterr!=ERROR_INVALID_HANDLE))
253 CloseHandle(m_hCompPort);
254 m_hCompPort = INVALID_HANDLE_VALUE;
256 // Since we pass m_hCompPort to CreateIoCompletionPort, we
257 // have to set this to NULL to have that API create a new
258 // handle.
259 m_hCompPort = NULL;
260 for (int i=0; i<watchedPaths.GetCount(); ++i)
262 CTGitPath watchedPath = watchedPaths[i];
264 HANDLE hDir = CreateFile(watchedPath.GetWinPath(),
265 FILE_LIST_DIRECTORY,
266 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
267 NULL, //security attributes
268 OPEN_EXISTING,
269 FILE_FLAG_BACKUP_SEMANTICS | //required privileges: SE_BACKUP_NAME and SE_RESTORE_NAME.
270 FILE_FLAG_OVERLAPPED,
271 NULL);
272 if (hDir == INVALID_HANDLE_VALUE)
274 // this could happen if a watched folder has been removed/renamed
275 ATLTRACE(_T("CDirectoryWatcher: CreateFile failed. Can't watch directory %s\n"), watchedPaths[i].GetWinPath());
276 CloseHandle(m_hCompPort);
277 m_hCompPort = INVALID_HANDLE_VALUE;
278 AutoLocker lock(m_critSec);
279 watchedPaths.RemovePath(watchedPath);
280 i--; if (i<0) i=0;
281 break;
284 DEV_BROADCAST_HANDLE NotificationFilter;
285 SecureZeroMemory(&NotificationFilter, sizeof(NotificationFilter));
286 NotificationFilter.dbch_size = sizeof(DEV_BROADCAST_HANDLE);
287 NotificationFilter.dbch_devicetype = DBT_DEVTYP_HANDLE;
288 NotificationFilter.dbch_handle = hDir;
289 NotificationFilter.dbch_hdevnotify = RegisterDeviceNotification(hWnd, &NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
291 CDirWatchInfo * pDirInfo = new CDirWatchInfo(hDir, watchedPath);
292 pDirInfo->m_hDevNotify = NotificationFilter.dbch_hdevnotify;
293 m_hCompPort = CreateIoCompletionPort(hDir, m_hCompPort, (ULONG_PTR)pDirInfo, 0);
294 if (m_hCompPort == NULL)
296 ATLTRACE(_T("CDirectoryWatcher: CreateIoCompletionPort failed. Can't watch directory %s\n"), watchedPath.GetWinPath());
297 AutoLocker lock(m_critSec);
298 ClearInfoMap();
299 delete pDirInfo;
300 pDirInfo = NULL;
301 watchedPaths.RemovePath(watchedPath);
302 i--; if (i<0) i=0;
303 break;
305 if (!ReadDirectoryChangesW(pDirInfo->m_hDir,
306 pDirInfo->m_Buffer,
307 READ_DIR_CHANGE_BUFFER_SIZE,
308 TRUE,
309 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
310 &numBytes,// not used
311 &pDirInfo->m_Overlapped,
312 NULL)) //no completion routine!
314 ATLTRACE(_T("CDirectoryWatcher: ReadDirectoryChangesW failed. Can't watch directory %s\n"), watchedPath.GetWinPath());
315 AutoLocker lock(m_critSec);
316 ClearInfoMap();
317 delete pDirInfo;
318 pDirInfo = NULL;
319 watchedPaths.RemovePath(watchedPath);
320 i--; if (i<0) i=0;
321 break;
323 AutoLocker lock(m_critSec);
324 watchInfoMap[pDirInfo->m_hDir] = pDirInfo;
325 ATLTRACE(_T("watching path %s\n"), pDirInfo->m_DirName.GetWinPath());
328 else
330 if (!m_bRunning)
331 return;
332 // NOTE: the longer this code takes to execute until ReadDirectoryChangesW
333 // is called again, the higher the chance that we miss some
334 // changes in the file system!
335 if (pdi)
337 if (numBytes == 0)
339 goto continuewatching;
341 PFILE_NOTIFY_INFORMATION pnotify = (PFILE_NOTIFY_INFORMATION)pdi->m_Buffer;
342 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
343 goto continuewatching;
344 DWORD nOffset = pnotify->NextEntryOffset;
348 nOffset = pnotify->NextEntryOffset;
349 if (pnotify->FileNameLength >= (READ_DIR_CHANGE_BUFFER_SIZE*sizeof(TCHAR)))
350 continue;
351 SecureZeroMemory(buf, READ_DIR_CHANGE_BUFFER_SIZE*sizeof(TCHAR));
352 _tcsncpy_s(buf, READ_DIR_CHANGE_BUFFER_SIZE, pdi->m_DirPath, READ_DIR_CHANGE_BUFFER_SIZE);
353 errno_t err = _tcsncat_s(buf+pdi->m_DirPath.GetLength(), READ_DIR_CHANGE_BUFFER_SIZE-pdi->m_DirPath.GetLength(), pnotify->FileName, _TRUNCATE);
354 if (err == STRUNCATE)
356 pnotify = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pnotify + nOffset);
357 continue;
359 buf[(pnotify->FileNameLength/sizeof(TCHAR))+pdi->m_DirPath.GetLength()] = 0;
360 pnotify = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pnotify + nOffset);
361 if (m_FolderCrawler)
363 if ((pFound = wcsstr(buf, L"\\tmp"))!=NULL)
365 pFound += 4;
366 if (((*pFound)=='\\')||((*pFound)=='\0'))
368 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
369 break;
370 continue;
373 if ((pFound = wcsstr(buf, L":\\RECYCLER\\"))!=NULL)
375 if ((pFound-buf) < 5)
377 // a notification for the recycle bin - ignore it
378 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
379 break;
380 continue;
383 if ((pFound = wcsstr(buf, L":\\$Recycle.Bin\\"))!=NULL)
385 if ((pFound-buf) < 5)
387 // a notification for the recycle bin - ignore it
388 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
389 break;
390 continue;
393 if ((pFound = wcsstr(buf, L".tmp"))!=NULL)
395 // assume files with a .tmp extension are not versioned and interesting,
396 // so ignore them.
397 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
398 break;
399 continue;
401 bool isIndex = false;
402 if ((pFound = wcsstr(buf, L".git"))!=NULL)
404 // omit repository data change except .git/index.lock- or .git/HEAD.lock-files
405 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
406 break;
408 if ((wcsstr(pFound, L"index.lock") != NULL && wcsstr(pFound, L"HEAD.lock") != NULL) && pnotify->Action == FILE_ACTION_ADDED)
410 m_FolderCrawler->BlockPath(CTGitPath(buf).GetContainingDirectory().GetContainingDirectory()); // optimize here, and use general BlockPath with priorities
411 continue;
413 else if ((wcsstr(pFound, L"index.lock") != NULL && wcsstr(pFound, L"HEAD.lock") != NULL) && pnotify->Action == FILE_ACTION_REMOVED)
415 isIndex = true;
416 m_FolderCrawler->BlockPath(CTGitPath(buf).GetContainingDirectory().GetContainingDirectory(), 1);
418 else
420 continue;
424 path.SetFromWin(buf);
425 if(!path.HasAdminDir() && !isIndex)
426 continue;
428 ATLTRACE(_T("change notification: %s\n"), buf);
429 m_FolderCrawler->AddPathForUpdate(CTGitPath(buf));
431 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
432 break;
433 } while (nOffset);
434 continuewatching:
435 SecureZeroMemory(pdi->m_Buffer, sizeof(pdi->m_Buffer));
436 SecureZeroMemory(&pdi->m_Overlapped, sizeof(OVERLAPPED));
437 if (!ReadDirectoryChangesW(pdi->m_hDir,
438 pdi->m_Buffer,
439 READ_DIR_CHANGE_BUFFER_SIZE,
440 TRUE,
441 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
442 &numBytes,// not used
443 &pdi->m_Overlapped,
444 NULL)) //no completion routine!
446 // Since the call to ReadDirectoryChangesW failed, just
447 // wait a while. We don't want to have this thread
448 // running using 100% CPU if something goes completely
449 // wrong.
450 Sleep(200);
454 } // if (watchedPaths.GetCount())
455 else
456 Sleep(200);
457 } // while (m_bRunning)
460 void CDirectoryWatcher::ClearInfoMap()
462 if (watchInfoMap.size()!=0)
464 AutoLocker lock(m_critSec);
465 for (std::map<HANDLE, CDirWatchInfo *>::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
467 CDirectoryWatcher::CDirWatchInfo * info = I->second;
468 delete info;
469 info = NULL;
472 watchInfoMap.clear();
473 if (m_hCompPort != INVALID_HANDLE_VALUE)
474 CloseHandle(m_hCompPort);
475 m_hCompPort = INVALID_HANDLE_VALUE;
478 CTGitPath CDirectoryWatcher::CloseInfoMap(HDEVNOTIFY hdev)
480 CTGitPath path;
481 if (watchInfoMap.size() == 0)
482 return path;
483 AutoLocker lock(m_critSec);
484 for (std::map<HANDLE, CDirWatchInfo *>::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
486 CDirectoryWatcher::CDirWatchInfo * info = I->second;
487 if (info->m_hDevNotify == hdev)
489 path = info->m_DirName;
490 RemovePathAndChildren(path);
491 BlockPath(path);
493 info->CloseDirectoryHandle();
495 watchInfoMap.clear();
496 if (m_hCompPort != INVALID_HANDLE_VALUE)
497 CloseHandle(m_hCompPort);
498 m_hCompPort = INVALID_HANDLE_VALUE;
499 return path;
502 bool CDirectoryWatcher::CloseHandlesForPath(const CTGitPath& path)
504 if (watchInfoMap.size() == 0)
505 return false;
506 AutoLocker lock(m_critSec);
507 for (std::map<HANDLE, CDirWatchInfo *>::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
509 CDirectoryWatcher::CDirWatchInfo * info = I->second;
510 CTGitPath p = CTGitPath(info->m_DirPath);
511 if (path.IsAncestorOf(p))
513 RemovePathAndChildren(p);
514 BlockPath(p);
516 info->CloseDirectoryHandle();
518 watchInfoMap.clear();
519 if (m_hCompPort != INVALID_HANDLE_VALUE)
520 CloseHandle(m_hCompPort);
521 m_hCompPort = INVALID_HANDLE_VALUE;
522 return true;
525 CDirectoryWatcher::CDirWatchInfo::CDirWatchInfo(HANDLE hDir, const CTGitPath& DirectoryName) :
526 m_hDir(hDir),
527 m_DirName(DirectoryName)
529 ATLASSERT( hDir != INVALID_HANDLE_VALUE
530 && !DirectoryName.IsEmpty());
531 memset(&m_Overlapped, 0, sizeof(m_Overlapped));
532 m_DirPath = m_DirName.GetWinPathString();
533 if (m_DirPath.GetAt(m_DirPath.GetLength()-1) != '\\')
534 m_DirPath += _T("\\");
535 m_hDevNotify = INVALID_HANDLE_VALUE;
538 CDirectoryWatcher::CDirWatchInfo::~CDirWatchInfo()
540 CloseDirectoryHandle();
543 bool CDirectoryWatcher::CDirWatchInfo::CloseDirectoryHandle()
545 bool b = TRUE;
546 if( m_hDir != INVALID_HANDLE_VALUE )
548 b = !!CloseHandle(m_hDir);
549 m_hDir = INVALID_HANDLE_VALUE;
551 if (m_hDevNotify != INVALID_HANDLE_VALUE)
553 UnregisterDeviceNotification(m_hDevNotify);
555 return b;