1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2008, 2011-2012 - TortoiseSVN
4 // Copyright (C) 2008-2014 - 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.
22 #include "GitStatusCache.h"
23 #include "DirectoryWatcher.h"
25 #include "SmartHandle.h"
30 extern CGitAdminDirMap g_AdminDirMap
;
32 CDirectoryWatcher::CDirectoryWatcher(void)
35 , m_FolderCrawler(NULL
)
38 // enable the required privileges for this process
40 LPCTSTR arPrivelegeNames
[] = { SE_BACKUP_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)
68 AutoLocker
lock(m_critSec
);
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();
95 void CDirectoryWatcher::Stop()
97 InterlockedExchange(&m_bRunning
, FALSE
);
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
);
113 for (int i
=0; i
<watchedPaths
.GetCount(); ++i
)
115 if (path
.IsAncestorOf(watchedPaths
[i
]))
117 watchedPaths
.RemovePath(watchedPaths
[i
]);
125 void CDirectoryWatcher::BlockPath(const CTGitPath
& path
)
128 // block the path from being watched for 4 seconds
129 blockTickCount
= GetTickCount()+4000;
130 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": Blocking path: %s\n"), path
.GetWinPath());
133 bool CDirectoryWatcher::AddPath(const CTGitPath
& path
, bool bCloseInfoMap
)
135 if (!CGitStatusCache::Instance().IsPathAllowed(path
))
137 if ((!blockedPath
.IsEmpty())&&(blockedPath
.IsAncestorOf(path
)))
139 if (GetTickCount() < blockTickCount
)
141 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": Path %s prevented from being watched\n"), path
.GetWinPath());
146 if (path
.GetWinPathString().Find(L
":\\RECYCLER\\") >= 0)
148 if (path
.GetWinPathString().Find(L
":\\$Recycle.Bin\\") >= 0)
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
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());
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
));
186 if (sPath
.GetLength() == minlen
)
188 if (watched
.GetLength() > minlen
)
190 if (watched
.GetAt(len
)=='\\')
194 else if (sPath
.GetLength() == 3 && sPath
[1] == ':')
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 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": add path to watch %s\n"), newroot
.GetWinPath());
219 watchedPaths
.AddPath(newroot
);
220 watchedPaths
.RemoveChildren();
226 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": add path to watch %s\n"), path
.GetWinPath());
227 watchedPaths
.AddPath(path
);
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
))
245 unsigned int CDirectoryWatcher::ThreadEntry(void* pContext
)
247 ((CDirectoryWatcher
*)pContext
)->WorkerThread();
251 void CDirectoryWatcher::WorkerThread()
254 CDirWatchInfo
* pdi
= NULL
;
255 LPOVERLAPPED lpOverlapped
;
256 WCHAR buf
[READ_DIR_CHANGE_BUFFER_SIZE
] = {0};
257 WCHAR
* pFound
= NULL
;
261 if (!watchedPaths
.IsEmpty())
263 // Any incoming notifications?
267 InterlockedExchange(&m_bCleaned
, FALSE
);
269 || !GetQueuedCompletionStatus(m_hCompPort
,
273 600000 /*10 minutes*/))
280 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _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
295 for (int i
=0; i
<watchedPaths
.GetCount(); ++i
)
297 CTGitPath watchedPath
= watchedPaths
[i
];
299 CAutoFile hDir
= CreateFile(watchedPath
.GetWinPath(),
301 FILE_SHARE_READ
| FILE_SHARE_WRITE
| FILE_SHARE_DELETE
,
302 NULL
, //security attributes
304 FILE_FLAG_BACKUP_SEMANTICS
| //required privileges: SE_BACKUP_NAME and SE_RESTORE_NAME.
305 FILE_FLAG_OVERLAPPED
,
309 // this could happen if a watched folder has been removed/renamed
310 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": CreateFile failed. Can't watch directory %s\n"), watchedPaths
[i
].GetWinPath());
311 watchedPaths
.RemovePath(watchedPath
);
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();
325 NotificationFilter
.dbch_hdevnotify
= RegisterDeviceNotification(hWnd
, &NotificationFilter
, DEVICE_NOTIFY_WINDOW_HANDLE
);
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()))
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);
347 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": 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();
358 watchedPaths
.RemovePath(watchedPath
);
363 if (!ReadDirectoryChangesW(pDirInfo
->m_hDir
,
365 READ_DIR_CHANGE_BUFFER_SIZE
,
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 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": 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();
382 watchedPaths
.RemovePath(watchedPath
);
386 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": watching path %s\n"), pDirInfo
->m_DirName
.GetWinPath());
387 watchInfoMap
[pDirInfo
->m_hDir
] = pDirInfo
;
394 if (watchInfoMap
.empty())
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!
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
))
411 if ( (!pdi
->m_hDir
) || watchInfoMap
.empty()
412 || (watchInfoMap
.find(pdi
->m_hDir
) == watchInfoMap
.end()))
416 PFILE_NOTIFY_INFORMATION pnotify
= (PFILE_NOTIFY_INFORMATION
)pdi
->m_Buffer
;
421 pnotify
= (PFILE_NOTIFY_INFORMATION
)((LPBYTE
)pnotify
+ nOffset
);
423 if ((ULONG_PTR
)pnotify
- (ULONG_PTR
)pdi
->m_Buffer
> READ_DIR_CHANGE_BUFFER_SIZE
)
426 nOffset
= pnotify
->NextEntryOffset
;
428 if (pnotify
->FileNameLength
>= (READ_DIR_CHANGE_BUFFER_SIZE
*sizeof(TCHAR
)))
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
, min(READ_DIR_CHANGE_BUFFER_SIZE
-pdi
->m_DirPath
.GetLength(), pnotify
->FileNameLength
/sizeof(TCHAR
)));
434 if (err
== STRUNCATE
)
438 buf
[(pnotify
->FileNameLength
/sizeof(TCHAR
))+pdi
->m_DirPath
.GetLength()] = 0;
442 if ((pFound
= wcsstr(buf
, L
"\\tmp")) != NULL
)
445 if (((*pFound
)=='\\')||((*pFound
)=='\0'))
450 if ((pFound
= wcsstr(buf
, L
":\\RECYCLER\\")) != NULL
)
452 if ((pFound
-buf
) < 5)
454 // a notification for the recycle bin - ignore it
458 if ((pFound
= wcsstr(buf
, L
":\\$Recycle.Bin\\")) != NULL
)
460 if ((pFound
-buf
) < 5)
462 // a notification for the recycle bin - ignore it
466 if (wcsstr(buf
, L
".tmp") != NULL
)
468 // assume files with a .tmp extension are not versioned and interesting,
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
)
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
);
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
))
491 CGitStatusCache::Instance().BlockPath(path
, 1);
499 path
.SetFromUnknown(buf
);
501 if(!path
.HasAdminDir() && !isIndex
)
504 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) _T(": change notification for %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
,
514 READ_DIR_CHANGE_BUFFER_SIZE
,
516 FILE_NOTIFY_CHANGE_FILE_NAME
| FILE_NOTIFY_CHANGE_DIR_NAME
| FILE_NOTIFY_CHANGE_LAST_WRITE
,
517 &numBytes
,// not used
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?
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
543 }// if (!watchedPaths.IsEmpty())
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
)
557 I
->second
->CloseDirectoryHandle();
559 CloseCompletionPort();
562 void CDirectoryWatcher::ClearInfoMap()
565 if (!watchInfoMap
.empty())
567 AutoLocker
lock(m_critSec
);
568 for (TInfoMap::iterator I
= watchInfoMap
.begin(); I
!= watchInfoMap
.end(); ++I
)
570 CDirectoryWatcher::CDirWatchInfo
* info
= I
->second
;
572 ScheduleForDeletion (info
);
574 watchInfoMap
.clear();
578 CTGitPath
CDirectoryWatcher::CloseInfoMap(HANDLE hDir
)
581 AutoLocker
lock(m_critSec
);
582 TInfoMap::const_iterator d
= watchInfoMap
.find(hDir
);
583 if (d
!= watchInfoMap
.end())
585 path
= CTGitPath(CTGitPath(d
->second
->m_DirPath
).GetRootPathString());
586 RemovePathAndChildren(path
);
591 if (watchInfoMap
.empty())
594 for (TInfoMap::iterator I
= watchInfoMap
.begin(); I
!= watchInfoMap
.end(); ++I
)
596 CDirectoryWatcher::CDirWatchInfo
* info
= I
->second
;
598 ScheduleForDeletion (info
);
600 watchInfoMap
.clear();
605 bool CDirectoryWatcher::CloseHandlesForPath(const CTGitPath
& path
)
607 AutoLocker
lock(m_critSec
);
610 if (watchInfoMap
.empty())
613 for (TInfoMap::iterator I
= watchInfoMap
.begin(); I
!= watchInfoMap
.end(); ++I
)
615 CDirectoryWatcher::CDirWatchInfo
* info
= I
->second
;
617 CTGitPath p
= CTGitPath(info
->m_DirPath
);
618 if (path
.IsAncestorOf(p
))
620 RemovePathAndChildren(p
);
623 ScheduleForDeletion(info
);
625 watchInfoMap
.clear();
629 CDirectoryWatcher::CDirWatchInfo::CDirWatchInfo(HANDLE hDir
, const CTGitPath
& DirectoryName
)
631 , m_DirName(DirectoryName
)
633 ATLASSERT(hDir
&& !DirectoryName
.IsEmpty());
635 SecureZeroMemory(&m_Overlapped
, sizeof(m_Overlapped
));
636 m_DirPath
= m_DirName
.GetWinPathString();
637 if (m_DirPath
.GetAt(m_DirPath
.GetLength()-1) != '\\')
638 m_DirPath
+= _T("\\");
642 CDirectoryWatcher::CDirWatchInfo::~CDirWatchInfo()
644 CloseDirectoryHandle();
647 bool CDirectoryWatcher::CDirWatchInfo::CloseDirectoryHandle()
649 bool b
= m_hDir
.CloseHandle();
653 UnregisterDeviceNotification(m_hDevNotify
);