TGitCache: Make sure paths do not have a trailing slash/backslash
[TortoiseGit.git] / src / TGitCache / DirectoryWatcher.cpp
blobd47e41c3ee86de0101b304bcb87b92124653a822
1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2008, 2011-2012 - TortoiseSVN
4 // Copyright (C) 2008-2012 - 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 hWnd;
30 extern CGitAdminDirMap g_AdminDirMap;
32 CDirectoryWatcher::CDirectoryWatcher(void)
33 : m_bRunning(TRUE)
34 , m_bCleaned(FALSE)
35 , m_FolderCrawler(NULL)
36 , blockTickCount(0)
38 // enable the required privileges for this process
40 LPCTSTR arPrivelegeNames[] = { SE_BACKUP_NAME,
41 SE_RESTORE_NAME,
42 SE_CHANGE_NOTIFY_NAME
45 for (int i=0; i<(sizeof(arPrivelegeNames)/sizeof(LPCTSTR)); ++i)
47 CAutoGeneralHandle hToken;
48 if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, hToken.GetPointer()))
50 TOKEN_PRIVILEGES tp = { 1 };
52 if (LookupPrivilegeValue(NULL, arPrivelegeNames[i], &tp.Privileges[0].Luid))
54 tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
56 AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
61 unsigned int threadId = 0;
62 m_hThread = (HANDLE)_beginthreadex(NULL,0,ThreadEntry,this,0,&threadId);
65 CDirectoryWatcher::~CDirectoryWatcher(void)
67 Stop();
68 AutoLocker lock(m_critSec);
69 ClearInfoMap();
70 CleanupWatchInfo();
73 void CDirectoryWatcher::CloseCompletionPort()
75 m_hCompPort.CloseHandle();
78 void CDirectoryWatcher::ScheduleForDeletion (CDirWatchInfo* info)
80 infoToDelete.push_back (info);
83 void CDirectoryWatcher::CleanupWatchInfo()
85 AutoLocker lock(m_critSec);
86 InterlockedExchange(&m_bCleaned, TRUE);
87 while (!infoToDelete.empty())
89 CDirWatchInfo* info = infoToDelete.back();
90 infoToDelete.pop_back();
91 delete info;
95 void CDirectoryWatcher::Stop()
97 InterlockedExchange(&m_bRunning, FALSE);
98 CloseWatchHandles();
99 WaitForSingleObject(m_hThread, 4000);
100 m_hThread.CloseHandle();
103 void CDirectoryWatcher::SetFolderCrawler(CFolderCrawler * crawler)
105 m_FolderCrawler = crawler;
108 bool CDirectoryWatcher::RemovePathAndChildren(const CTGitPath& path)
110 bool bRemoved = false;
111 AutoLocker lock(m_critSec);
112 repeat:
113 for (int i=0; i<watchedPaths.GetCount(); ++i)
115 if (path.IsAncestorOf(watchedPaths[i]))
117 watchedPaths.RemovePath(watchedPaths[i]);
118 bRemoved = true;
119 goto repeat;
122 return bRemoved;
125 void CDirectoryWatcher::BlockPath(const CTGitPath& path)
127 blockedPath = path;
128 // block the path from being watched for 4 seconds
129 blockTickCount = GetTickCount()+4000;
130 ATLTRACE(_T("Blocking path: %s\n"), path.GetWinPath());
133 bool CDirectoryWatcher::AddPath(const CTGitPath& path, bool bCloseInfoMap)
135 if (!CGitStatusCache::Instance().IsPathAllowed(path))
136 return false;
137 if ((!blockedPath.IsEmpty())&&(blockedPath.IsAncestorOf(path)))
139 if (GetTickCount() < blockTickCount)
141 ATLTRACE(_T("Path %s prevented from being watched\n"), path.GetWinPath());
142 return false;
146 if (path.GetWinPathString().Find(L":\\RECYCLER\\") >= 0)
147 return false;
148 if (path.GetWinPathString().Find(L":\\$Recycle.Bin\\") >= 0)
149 return false;
151 AutoLocker lock(m_critSec);
152 for (int i=0; i<watchedPaths.GetCount(); ++i)
154 if (watchedPaths[i].IsAncestorOf(path))
155 return false; // already watched (recursively)
158 // now check if with the new path we might have a new root
159 CTGitPath newroot;
160 for (int i=0; i<watchedPaths.GetCount(); ++i)
162 const CString& watched = watchedPaths[i].GetWinPathString();
163 const CString& sPath = path.GetWinPathString();
164 int minlen = min(sPath.GetLength(), watched.GetLength());
165 int len = 0;
166 for (len = 0; len < minlen; ++len)
168 if (watched.GetAt(len) != sPath.GetAt(len))
170 if ((len > 1)&&(len < minlen))
172 if (sPath.GetAt(len)=='\\')
174 newroot = CTGitPath(sPath.Left(len));
176 else if (watched.GetAt(len)=='\\')
178 newroot = CTGitPath(watched.Left(len));
181 break;
184 if (len == minlen)
186 if (sPath.GetLength() == minlen)
188 if (watched.GetLength() > minlen)
190 if (watched.GetAt(len)=='\\')
192 newroot = path;
194 else if (sPath.GetLength() == 3 && sPath[1] == ':')
196 newroot = path;
200 else
202 if (sPath.GetLength() > minlen)
204 if (sPath.GetAt(len)=='\\')
206 newroot = CTGitPath(watched);
208 else if (watched.GetLength() == 3 && watched[1] == ':')
210 newroot = CTGitPath(watched);
216 if (!newroot.IsEmpty())
218 ATLTRACE(_T("add path to watch %s\n"), newroot.GetWinPath());
219 watchedPaths.AddPath(newroot);
220 watchedPaths.RemoveChildren();
221 if (bCloseInfoMap)
222 ClearInfoMap();
224 return true;
226 ATLTRACE(_T("add path to watch %s\n"), path.GetWinPath());
227 watchedPaths.AddPath(path);
228 if (bCloseInfoMap)
229 ClearInfoMap();
231 return true;
234 bool CDirectoryWatcher::IsPathWatched(const CTGitPath& path)
236 AutoLocker lock(m_critSec);
237 for (int i=0; i<watchedPaths.GetCount(); ++i)
239 if (watchedPaths[i].IsAncestorOf(path))
240 return true;
242 return false;
245 unsigned int CDirectoryWatcher::ThreadEntry(void* pContext)
247 ((CDirectoryWatcher*)pContext)->WorkerThread();
248 return 0;
251 void CDirectoryWatcher::WorkerThread()
253 DWORD numBytes;
254 CDirWatchInfo * pdi = NULL;
255 LPOVERLAPPED lpOverlapped;
256 WCHAR buf[READ_DIR_CHANGE_BUFFER_SIZE] = {0};
257 WCHAR * pFound = NULL;
258 while (m_bRunning)
260 CleanupWatchInfo();
261 if (watchedPaths.GetCount())
263 // Any incoming notifications?
265 pdi = NULL;
266 numBytes = 0;
267 InterlockedExchange(&m_bCleaned, FALSE);
268 if ((!m_hCompPort)
269 || !GetQueuedCompletionStatus(m_hCompPort,
270 &numBytes,
271 (PULONG_PTR) &pdi,
272 &lpOverlapped,
273 600000 /*10 minutes*/))
275 // No. Still trying?
277 if (!m_bRunning)
278 return;
280 ATLTRACE(_T(": restarting watcher\n"));
281 m_hCompPort.CloseHandle();
283 // We must sync the whole section because other threads may
284 // receive "AddPath" calls that will delete the completion
285 // port *while* we are adding references to it .
287 AutoLocker lock(m_critSec);
289 // Clear the list of watched objects and recreate that list.
290 // This will also delete the old completion port
292 ClearInfoMap();
293 CleanupWatchInfo();
295 for (int i=0; i<watchedPaths.GetCount(); ++i)
297 CTGitPath watchedPath = watchedPaths[i];
299 CAutoFile hDir = CreateFile(watchedPath.GetWinPath(),
300 FILE_LIST_DIRECTORY,
301 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
302 NULL, //security attributes
303 OPEN_EXISTING,
304 FILE_FLAG_BACKUP_SEMANTICS | //required privileges: SE_BACKUP_NAME and SE_RESTORE_NAME.
305 FILE_FLAG_OVERLAPPED,
306 NULL);
307 if (!hDir)
309 // this could happen if a watched folder has been removed/renamed
310 ATLTRACE(_T("CDirectoryWatcher: CreateFile failed. Can't watch directory %s\n"), watchedPaths[i].GetWinPath());
311 watchedPaths.RemovePath(watchedPath);
312 break;
315 DEV_BROADCAST_HANDLE NotificationFilter;
316 SecureZeroMemory(&NotificationFilter, sizeof(NotificationFilter));
317 NotificationFilter.dbch_size = sizeof(DEV_BROADCAST_HANDLE);
318 NotificationFilter.dbch_devicetype = DBT_DEVTYP_HANDLE;
319 NotificationFilter.dbch_handle = hDir;
320 // RegisterDeviceNotification sends a message to the UI thread:
321 // make sure we *can* send it and that the UI thread isn't waiting on a lock
322 int numPaths = watchedPaths.GetCount();
323 size_t numWatch = watchInfoMap.size();
324 lock.Unlock();
325 NotificationFilter.dbch_hdevnotify = RegisterDeviceNotification(hWnd, &NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
326 lock.Lock();
327 // since we released the lock to prevent a deadlock with the UI thread,
328 // it could happen that new paths were added to watch, or another thread
329 // could have cleared our info map.
330 // if that happened, we have to restart watching all paths again.
331 if ((numPaths != watchedPaths.GetCount()) || (numWatch != watchInfoMap.size()))
333 ClearInfoMap();
334 CleanupWatchInfo();
335 Sleep(200);
336 break;
339 CDirWatchInfo * pDirInfo = new CDirWatchInfo(hDir, watchedPath);
340 hDir.Detach(); // the new CDirWatchInfo object owns the handle now
341 pDirInfo->m_hDevNotify = NotificationFilter.dbch_hdevnotify;
344 HANDLE port = CreateIoCompletionPort(pDirInfo->m_hDir, m_hCompPort, (ULONG_PTR)pDirInfo, 0);
345 if (port == NULL)
347 ATLTRACE(_T("CDirectoryWatcher: CreateIoCompletionPort failed. Can't watch directory %s\n"), watchedPath.GetWinPath());
349 // we must close the directory handle to allow ClearInfoMap()
350 // to close the completion port properly
351 pDirInfo->CloseDirectoryHandle();
353 ClearInfoMap();
354 CleanupWatchInfo();
355 delete pDirInfo;
356 pDirInfo = NULL;
358 watchedPaths.RemovePath(watchedPath);
359 break;
361 m_hCompPort = port;
363 if (!ReadDirectoryChangesW(pDirInfo->m_hDir,
364 pDirInfo->m_Buffer,
365 READ_DIR_CHANGE_BUFFER_SIZE,
366 TRUE,
367 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
368 &numBytes,// not used
369 &pDirInfo->m_Overlapped,
370 NULL)) //no completion routine!
372 ATLTRACE(_T("CDirectoryWatcher: ReadDirectoryChangesW failed. Can't watch directory %s\n"), watchedPath.GetWinPath());
374 // we must close the directory handle to allow ClearInfoMap()
375 // to close the completion port properly
376 pDirInfo->CloseDirectoryHandle();
378 ClearInfoMap();
379 CleanupWatchInfo();
380 delete pDirInfo;
381 pDirInfo = NULL;
382 watchedPaths.RemovePath(watchedPath);
383 break;
386 ATLTRACE(_T("watching path %s\n"), pDirInfo->m_DirName.GetWinPath());
387 watchInfoMap[pDirInfo->m_hDir] = pDirInfo;
390 else
392 if (!m_bRunning)
393 return;
394 if (watchInfoMap.empty())
395 continue;
397 // NOTE: the longer this code takes to execute until ReadDirectoryChangesW
398 // is called again, the higher the chance that we miss some
399 // changes in the file system!
400 if (pdi)
402 BOOL bRet = false;
403 std::list<CTGitPath> notifyPaths;
405 AutoLocker lock(m_critSec);
406 // in case the CDirectoryWatcher objects have been cleaned,
407 // the m_bCleaned variable will be set to true here. If the
408 // objects haven't been cleared, we can access them here.
409 if (InterlockedExchange(&m_bCleaned, FALSE))
410 continue;
411 if ( (!pdi->m_hDir) || watchInfoMap.empty()
412 || (watchInfoMap.find(pdi->m_hDir) == watchInfoMap.end()))
414 continue;
416 PFILE_NOTIFY_INFORMATION pnotify = (PFILE_NOTIFY_INFORMATION)pdi->m_Buffer;
417 DWORD nOffset = 0;
421 pnotify = (PFILE_NOTIFY_INFORMATION)((LPBYTE)pnotify + nOffset);
423 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
424 break;
426 nOffset = pnotify->NextEntryOffset;
428 if (pnotify->FileNameLength >= (READ_DIR_CHANGE_BUFFER_SIZE*sizeof(TCHAR)))
429 continue;
431 SecureZeroMemory(buf, READ_DIR_CHANGE_BUFFER_SIZE*sizeof(TCHAR));
432 _tcsncpy_s(buf, pdi->m_DirPath, _countof(buf) - 1);
433 errno_t err = _tcsncat_s(buf+pdi->m_DirPath.GetLength(), READ_DIR_CHANGE_BUFFER_SIZE-pdi->m_DirPath.GetLength(), pnotify->FileName, _TRUNCATE);
434 if (err == STRUNCATE)
436 continue;
438 buf[(pnotify->FileNameLength/sizeof(TCHAR))+pdi->m_DirPath.GetLength()] = 0;
440 if (m_FolderCrawler)
442 if ((pFound = wcsstr(buf, L"\\tmp")) != NULL)
444 pFound += 4;
445 if (((*pFound)=='\\')||((*pFound)=='\0'))
447 continue;
450 if ((pFound = wcsstr(buf, L":\\RECYCLER\\")) != NULL)
452 if ((pFound-buf) < 5)
454 // a notification for the recycle bin - ignore it
455 continue;
458 if ((pFound = wcsstr(buf, L":\\$Recycle.Bin\\")) != NULL)
460 if ((pFound-buf) < 5)
462 // a notification for the recycle bin - ignore it
463 continue;
466 if (wcsstr(buf, L".tmp") != NULL)
468 // assume files with a .tmp extension are not versioned and interesting,
469 // so ignore them.
470 continue;
473 CTGitPath path;
474 bool isIndex = false;
475 if ((pFound = wcsstr(buf, L".git")) != NULL)
477 // omit repository data change except .git/index.lock- or .git/HEAD.lock-files
478 if ((ULONG_PTR)pnotify - (ULONG_PTR)pdi->m_Buffer > READ_DIR_CHANGE_BUFFER_SIZE)
479 break;
481 path = g_AdminDirMap.GetWorkingCopy(CTGitPath(buf).GetContainingDirectory().GetWinPathString());
483 if ((wcsstr(pFound, L"index.lock") != NULL || wcsstr(pFound, L"HEAD.lock") != NULL) && pnotify->Action == FILE_ACTION_ADDED)
485 CGitStatusCache::Instance().BlockPath(path);
486 continue;
488 else if (((wcsstr(pFound, L"index.lock") != NULL || wcsstr(pFound, L"HEAD.lock") != NULL) && pnotify->Action == FILE_ACTION_REMOVED) || (((wcsstr(pFound, L"index") != NULL && wcsstr(pFound, L"index.lock") == NULL) || (wcsstr(pFound, L"HEAD") != NULL && wcsstr(pFound, L"HEAD.lock") != NULL)) && pnotify->Action == FILE_ACTION_MODIFIED) || ((wcsstr(pFound, L"index.lock") == NULL || wcsstr(pFound, L"HEAD.lock") != NULL) && pnotify->Action == FILE_ACTION_RENAMED_NEW_NAME))
490 isIndex = true;
491 CGitStatusCache::Instance().BlockPath(path, 1);
493 else
495 continue;
498 else
499 path.SetFromUnknown(buf);
501 if(!path.HasAdminDir() && !isIndex)
502 continue;
504 ATLTRACE(_T("change notification: %s\n"), buf);
505 notifyPaths.push_back(path);
507 } while ((nOffset > 0)&&(nOffset < READ_DIR_CHANGE_BUFFER_SIZE));
509 // setup next notification cycle
510 SecureZeroMemory (pdi->m_Buffer, sizeof(pdi->m_Buffer));
511 SecureZeroMemory (&pdi->m_Overlapped, sizeof(OVERLAPPED));
512 bRet = ReadDirectoryChangesW(pdi->m_hDir,
513 pdi->m_Buffer,
514 READ_DIR_CHANGE_BUFFER_SIZE,
515 TRUE,
516 FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
517 &numBytes,// not used
518 &pdi->m_Overlapped,
519 NULL); //no completion routine!
521 if (!notifyPaths.empty())
523 for (std::list<CTGitPath>::const_iterator nit = notifyPaths.begin(); nit != notifyPaths.end(); ++nit)
525 m_FolderCrawler->AddPathForUpdate(*nit);
529 // any clean-up to do?
531 CleanupWatchInfo();
533 if (!bRet)
535 // Since the call to ReadDirectoryChangesW failed, just
536 // wait a while. We don't want to have this thread
537 // running using 100% CPU if something goes completely
538 // wrong.
539 Sleep(200);
543 }// if (watchedPaths.GetCount())
544 else
545 Sleep(200);
546 }// while (m_bRunning)
549 // call this before destroying async I/O structures:
551 void CDirectoryWatcher::CloseWatchHandles()
553 AutoLocker lock(m_critSec);
555 for (TInfoMap::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
556 I->second->CloseDirectoryHandle();
558 CloseCompletionPort();
561 void CDirectoryWatcher::ClearInfoMap()
563 CloseWatchHandles();
564 if (!watchInfoMap.empty())
566 AutoLocker lock(m_critSec);
567 for (TInfoMap::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
569 CDirectoryWatcher::CDirWatchInfo * info = I->second;
570 I->second = NULL;
571 ScheduleForDeletion (info);
573 watchInfoMap.clear();
577 CTGitPath CDirectoryWatcher::CloseInfoMap(HANDLE hDir)
579 AutoLocker lock(m_critSec);
580 TInfoMap::const_iterator d = watchInfoMap.find(hDir);
581 if (d != watchInfoMap.end())
583 CTGitPath root = CTGitPath(CTGitPath(d->second->m_DirPath).GetRootPathString());
584 RemovePathAndChildren(root);
585 BlockPath(root);
587 CloseWatchHandles();
589 CTGitPath path;
590 if (watchInfoMap.empty())
591 return path;
593 for (TInfoMap::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
595 CDirectoryWatcher::CDirWatchInfo * info = I->second;
597 ScheduleForDeletion (info);
599 watchInfoMap.clear();
601 return path;
604 bool CDirectoryWatcher::CloseHandlesForPath(const CTGitPath& path)
606 AutoLocker lock(m_critSec);
607 CloseWatchHandles();
609 if (watchInfoMap.empty())
610 return false;
612 for (TInfoMap::iterator I = watchInfoMap.begin(); I != watchInfoMap.end(); ++I)
614 CDirectoryWatcher::CDirWatchInfo * info = I->second;
615 I->second = NULL;
616 CTGitPath p = CTGitPath(info->m_DirPath);
617 if (path.IsAncestorOf(p))
619 RemovePathAndChildren(p);
620 BlockPath(p);
622 ScheduleForDeletion(info);
624 watchInfoMap.clear();
625 return true;
628 CDirectoryWatcher::CDirWatchInfo::CDirWatchInfo(HANDLE hDir, const CTGitPath& DirectoryName)
629 : m_hDir(hDir)
630 , m_DirName(DirectoryName)
632 ATLASSERT(hDir && !DirectoryName.IsEmpty());
633 m_Buffer[0] = 0;
634 SecureZeroMemory(&m_Overlapped, sizeof(m_Overlapped));
635 m_DirPath = m_DirName.GetWinPathString();
636 if (m_DirPath.GetAt(m_DirPath.GetLength()-1) != '\\')
637 m_DirPath += _T("\\");
638 m_hDevNotify = NULL;
641 CDirectoryWatcher::CDirWatchInfo::~CDirWatchInfo()
643 CloseDirectoryHandle();
646 bool CDirectoryWatcher::CDirWatchInfo::CloseDirectoryHandle()
648 bool b = m_hDir.CloseHandle();
650 if (m_hDevNotify)
652 UnregisterDeviceNotification(m_hDevNotify);
653 m_hDevNotify = NULL;
655 return b;