1 // TortoiseSVN - a Windows shell extension for easy version control
\r
3 // External Cache Copyright (C) 2005-2008 - TortoiseSVN
\r
5 // This program is free software; you can redistribute it and/or
\r
6 // modify it under the terms of the GNU General Public License
\r
7 // as published by the Free Software Foundation; either version 2
\r
8 // of the License, or (at your option) any later version.
\r
10 // This program is distributed in the hope that it will be useful,
\r
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
\r
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
\r
13 // GNU General Public License for more details.
\r
15 // You should have received a copy of the GNU General Public License
\r
16 // along with this program; if not, write to the Free Software Foundation,
\r
17 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
\r
21 #include ".\foldercrawler.h"
\r
22 #include "GitStatusCache.h"
\r
23 #include "registry.h"
\r
24 #include "TSVNCache.h"
\r
28 CFolderCrawler::CFolderCrawler(void)
\r
30 m_hWakeEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
\r
31 m_hTerminationEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
\r
32 m_hThread = INVALID_HANDLE_VALUE;
\r
33 m_lCrawlInhibitSet = 0;
\r
34 m_crawlHoldoffReleasesAt = (long)GetTickCount();
\r
36 m_bPathsAddedSinceLastCrawl = false;
\r
37 m_bItemsAddedSinceLastCrawl = false;
\r
40 CFolderCrawler::~CFolderCrawler(void)
\r
45 void CFolderCrawler::Stop()
\r
48 if (m_hTerminationEvent != INVALID_HANDLE_VALUE)
\r
50 SetEvent(m_hTerminationEvent);
\r
51 if(WaitForSingleObject(m_hThread, 4000) != WAIT_OBJECT_0)
\r
53 ATLTRACE("Error terminating crawler thread\n");
\r
55 CloseHandle(m_hThread);
\r
56 m_hThread = INVALID_HANDLE_VALUE;
\r
57 CloseHandle(m_hTerminationEvent);
\r
58 m_hTerminationEvent = INVALID_HANDLE_VALUE;
\r
59 CloseHandle(m_hWakeEvent);
\r
60 m_hWakeEvent = INVALID_HANDLE_VALUE;
\r
64 void CFolderCrawler::Initialise()
\r
66 // Don't call Initialize more than once
\r
67 ATLASSERT(m_hThread == INVALID_HANDLE_VALUE);
\r
69 // Just start the worker thread.
\r
70 // It will wait for event being signaled.
\r
71 // If m_hWakeEvent is already signaled the worker thread
\r
72 // will behave properly (with normal priority at worst).
\r
75 unsigned int threadId;
\r
76 m_hThread = (HANDLE)_beginthreadex(NULL,0,ThreadEntry,this,0,&threadId);
\r
77 SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE);
\r
80 void CFolderCrawler::AddDirectoryForUpdate(const CTGitPath& path)
\r
82 if (!CGitStatusCache::Instance().IsPathGood(path))
\r
85 AutoLocker lock(m_critSec);
\r
86 m_foldersToUpdate.push_back(path);
\r
87 m_foldersToUpdate.back().SetCustomData(GetTickCount()+10);
\r
88 ATLASSERT(path.IsDirectory() || !path.Exists());
\r
89 // set this flag while we are sync'ed
\r
90 // with the worker thread
\r
91 m_bItemsAddedSinceLastCrawl = true;
\r
94 SetEvent(m_hWakeEvent);
\r
97 void CFolderCrawler::AddPathForUpdate(const CTGitPath& path)
\r
99 if (!CGitStatusCache::Instance().IsPathGood(path))
\r
102 AutoLocker lock(m_critSec);
\r
103 m_pathsToUpdate.push_back(path);
\r
104 m_pathsToUpdate.back().SetCustomData(GetTickCount()+1000);
\r
105 m_bPathsAddedSinceLastCrawl = true;
\r
108 SetEvent(m_hWakeEvent);
\r
111 unsigned int CFolderCrawler::ThreadEntry(void* pContext)
\r
113 ((CFolderCrawler*)pContext)->WorkerThread();
\r
117 void CFolderCrawler::WorkerThread()
\r
119 HANDLE hWaitHandles[2];
\r
120 hWaitHandles[0] = m_hTerminationEvent;
\r
121 hWaitHandles[1] = m_hWakeEvent;
\r
122 CTGitPath workingPath;
\r
123 bool bFirstRunAfterWakeup = false;
\r
124 DWORD currentTicks = 0;
\r
126 // Quick check if we're on Vista
\r
127 OSVERSIONINFOEX inf;
\r
128 SecureZeroMemory(&inf, sizeof(OSVERSIONINFOEX));
\r
129 inf.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
\r
130 GetVersionEx((OSVERSIONINFO *)&inf);
\r
131 WORD fullver = MAKEWORD(inf.dwMinorVersion, inf.dwMajorVersion);
\r
135 bool bRecursive = !!(DWORD)CRegStdWORD(_T("Software\\TortoiseGit\\RecursiveOverlay"), TRUE);
\r
137 if (fullver >= 0x0600)
\r
139 SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_END);
\r
141 DWORD waitResult = WaitForMultipleObjects(sizeof(hWaitHandles)/sizeof(hWaitHandles[0]), hWaitHandles, FALSE, INFINITE);
\r
143 // exit event/working loop if the first event (m_hTerminationEvent)
\r
144 // has been signaled or if one of the events has been abandoned
\r
145 // (i.e. ~CFolderCrawler() is being executed)
\r
146 if(m_bRun == false || waitResult == WAIT_OBJECT_0 || waitResult == WAIT_ABANDONED_0 || waitResult == WAIT_ABANDONED_0+1)
\r
148 // Termination event
\r
152 if (fullver >= 0x0600)
\r
154 SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_BEGIN);
\r
157 // If we get here, we've been woken up by something being added to the queue.
\r
158 // However, it's important that we don't do our crawling while
\r
159 // the shell is still asking for items
\r
161 bFirstRunAfterWakeup = true;
\r
166 // Any locks today?
\r
167 if (CGitStatusCache::Instance().m_bClearMemory)
\r
169 CGitStatusCache::Instance().WaitToWrite();
\r
170 CGitStatusCache::Instance().ClearCache();
\r
171 CGitStatusCache::Instance().Done();
\r
172 CGitStatusCache::Instance().m_bClearMemory = false;
\r
174 if(m_lCrawlInhibitSet > 0)
\r
176 // We're in crawl hold-off
\r
177 ATLTRACE("Crawl hold-off\n");
\r
181 if (bFirstRunAfterWakeup)
\r
184 bFirstRunAfterWakeup = false;
\r
187 if ((m_blockReleasesAt < GetTickCount())&&(!m_blockedPath.IsEmpty()))
\r
189 ATLTRACE(_T("stop blocking path %s\n"), m_blockedPath.GetWinPath());
\r
190 m_blockedPath.Reset();
\r
193 if ((m_foldersToUpdate.empty())&&(m_pathsToUpdate.empty()))
\r
195 // Nothing left to do
\r
198 currentTicks = GetTickCount();
\r
199 if (!m_pathsToUpdate.empty())
\r
202 AutoLocker lock(m_critSec);
\r
204 if (m_bPathsAddedSinceLastCrawl)
\r
206 // The queue has changed - it's worth sorting and de-duping
\r
207 std::sort(m_pathsToUpdate.begin(), m_pathsToUpdate.end());
\r
208 m_pathsToUpdate.erase(std::unique(m_pathsToUpdate.begin(), m_pathsToUpdate.end(), &CTGitPath::PredLeftSameWCPathAsRight), m_pathsToUpdate.end());
\r
209 m_bPathsAddedSinceLastCrawl = false;
\r
211 workingPath = m_pathsToUpdate.front();
\r
212 if ((DWORD(workingPath.GetCustomData()) < currentTicks)||(DWORD(workingPath.GetCustomData()) > (currentTicks + 200000)))
\r
213 m_pathsToUpdate.pop_front();
\r
216 // since we always sort the whole list, we risk adding tons of new paths to m_pathsToUpdate
\r
217 // until the last one in the sorted list finally times out.
\r
218 // to avoid that, we go through the list again and crawl the first one which is valid
\r
219 // to crawl. That way, we still reduce the size of the list.
\r
220 if (m_pathsToUpdate.size() > 10)
\r
221 ATLTRACE("attention: the list of paths to update is now %ld big!\n", m_pathsToUpdate.size());
\r
222 for (std::deque<CTGitPath>::iterator it = m_pathsToUpdate.begin(); it != m_pathsToUpdate.end(); ++it)
\r
225 if ((DWORD(workingPath.GetCustomData()) < currentTicks)||(DWORD(workingPath.GetCustomData()) > (currentTicks + 200000)))
\r
227 m_pathsToUpdate.erase(it);
\r
233 if (DWORD(workingPath.GetCustomData()) >= currentTicks)
\r
238 if ((!m_blockedPath.IsEmpty())&&(m_blockedPath.IsAncestorOf(workingPath)))
\r
240 // don't crawl paths that are excluded
\r
241 if (!CGitStatusCache::Instance().IsPathAllowed(workingPath))
\r
243 // check if the changed path is inside an .svn folder
\r
244 if ((workingPath.HasAdminDir()&&workingPath.IsDirectory())||workingPath.IsAdminDir())
\r
246 // we don't crawl for paths changed in a tmp folder inside an .svn folder.
\r
247 // Because we also get notifications for those even if we just ask for the status!
\r
248 // And changes there don't affect the file status at all, so it's safe
\r
249 // to ignore notifications on those paths.
\r
250 if (workingPath.IsAdminDir())
\r
252 CString lowerpath = workingPath.GetWinPathString();
\r
253 lowerpath.MakeLower();
\r
254 if (lowerpath.Find(_T("\\tmp\\"))>0)
\r
256 if (lowerpath.Find(_T("\\tmp")) == (lowerpath.GetLength()-4))
\r
258 if (lowerpath.Find(_T("\\log"))>0)
\r
260 // Here's a little problem:
\r
261 // the lock file is also created for fetching the status
\r
262 // and not just when committing.
\r
263 // If we could find out why the lock file was changed
\r
264 // we could decide to crawl the folder again or not.
\r
265 // But for now, we have to crawl the parent folder
\r
268 //if (lowerpath.Find(_T("\\lock"))>0)
\r
271 else if (!workingPath.Exists())
\r
273 CGitStatusCache::Instance().WaitToWrite();
\r
274 CGitStatusCache::Instance().RemoveCacheForPath(workingPath);
\r
275 CGitStatusCache::Instance().Done();
\r
281 workingPath = workingPath.GetContainingDirectory();
\r
282 } while(workingPath.IsAdminDir());
\r
284 ATLTRACE(_T("Invalidating and refreshing folder: %s\n"), workingPath.GetWinPath());
\r
286 AutoLocker print(critSec);
\r
287 _stprintf_s(szCurrentCrawledPath[nCurrentCrawledpathIndex], MAX_CRAWLEDPATHSLEN, _T("Invalidating and refreshing folder: %s"), workingPath.GetWinPath());
\r
288 nCurrentCrawledpathIndex++;
\r
289 if (nCurrentCrawledpathIndex >= MAX_CRAWLEDPATHS)
\r
290 nCurrentCrawledpathIndex = 0;
\r
292 InvalidateRect(hWnd, NULL, FALSE);
\r
293 CGitStatusCache::Instance().WaitToRead();
\r
294 // Invalidate the cache of this folder, to make sure its status is fetched again.
\r
295 CCachedDirectory * pCachedDir = CGitStatusCache::Instance().GetDirectoryCacheEntry(workingPath);
\r
298 git_wc_status_kind status = pCachedDir->GetCurrentFullStatus();
\r
299 pCachedDir->Invalidate();
\r
300 if (workingPath.Exists())
\r
302 pCachedDir->RefreshStatus(bRecursive);
\r
303 // if the previous status wasn't normal and now it is, then
\r
304 // send a notification too.
\r
305 // We do this here because GetCurrentFullStatus() doesn't send
\r
306 // notifications for 'normal' status - if it would, we'd get tons
\r
307 // of notifications when crawling a working copy not yet in the cache.
\r
308 if ((status != git_wc_status_normal)&&(pCachedDir->GetCurrentFullStatus() != status))
\r
310 CGitStatusCache::Instance().UpdateShell(workingPath);
\r
311 ATLTRACE(_T("shell update in crawler for %s\n"), workingPath.GetWinPath());
\r
316 CGitStatusCache::Instance().Done();
\r
317 CGitStatusCache::Instance().WaitToWrite();
\r
318 CGitStatusCache::Instance().RemoveCacheForPath(workingPath);
\r
321 CGitStatusCache::Instance().Done();
\r
322 //In case that svn_client_stat() modified a file and we got
\r
323 //a notification about that in the directory watcher,
\r
324 //remove that here again - this is to prevent an endless loop
\r
325 AutoLocker lock(m_critSec);
\r
326 m_pathsToUpdate.erase(std::remove(m_pathsToUpdate.begin(), m_pathsToUpdate.end(), workingPath), m_pathsToUpdate.end());
\r
328 else if (workingPath.HasAdminDir())
\r
330 if (!workingPath.Exists())
\r
332 CGitStatusCache::Instance().WaitToWrite();
\r
333 CGitStatusCache::Instance().RemoveCacheForPath(workingPath);
\r
334 CGitStatusCache::Instance().Done();
\r
337 if (!workingPath.Exists())
\r
339 ATLTRACE(_T("Updating path: %s\n"), workingPath.GetWinPath());
\r
341 AutoLocker print(critSec);
\r
342 _stprintf_s(szCurrentCrawledPath[nCurrentCrawledpathIndex], MAX_CRAWLEDPATHSLEN, _T("Updating path: %s"), workingPath.GetWinPath());
\r
343 nCurrentCrawledpathIndex++;
\r
344 if (nCurrentCrawledpathIndex >= MAX_CRAWLEDPATHS)
\r
345 nCurrentCrawledpathIndex = 0;
\r
347 InvalidateRect(hWnd, NULL, FALSE);
\r
348 // HasAdminDir() already checks if the path points to a dir
\r
349 DWORD flags = TSVNCACHE_FLAGS_FOLDERISKNOWN;
\r
350 flags |= (workingPath.IsDirectory() ? TSVNCACHE_FLAGS_ISFOLDER : 0);
\r
351 flags |= (bRecursive ? TSVNCACHE_FLAGS_RECUSIVE_STATUS : 0);
\r
352 CGitStatusCache::Instance().WaitToRead();
\r
353 // Invalidate the cache of folders manually. The cache of files is invalidated
\r
354 // automatically if the status is asked for it and the file times don't match
\r
355 // anymore, so we don't need to manually invalidate those.
\r
356 if (workingPath.IsDirectory())
\r
358 CCachedDirectory * cachedDir = CGitStatusCache::Instance().GetDirectoryCacheEntry(workingPath);
\r
360 cachedDir->Invalidate();
\r
362 CStatusCacheEntry ce = CGitStatusCache::Instance().GetStatusForPath(workingPath, flags);
\r
363 if (ce.GetEffectiveStatus() > git_wc_status_unversioned)
\r
365 CGitStatusCache::Instance().UpdateShell(workingPath);
\r
366 ATLTRACE(_T("shell update in folder crawler for %s\n"), workingPath.GetWinPath());
\r
368 CGitStatusCache::Instance().Done();
\r
369 AutoLocker lock(m_critSec);
\r
370 m_pathsToUpdate.erase(std::remove(m_pathsToUpdate.begin(), m_pathsToUpdate.end(), workingPath), m_pathsToUpdate.end());
\r
374 if (!workingPath.Exists())
\r
376 CGitStatusCache::Instance().WaitToWrite();
\r
377 CGitStatusCache::Instance().RemoveCacheForPath(workingPath);
\r
378 CGitStatusCache::Instance().Done();
\r
382 else if (!m_foldersToUpdate.empty())
\r
385 AutoLocker lock(m_critSec);
\r
387 if (m_bItemsAddedSinceLastCrawl)
\r
389 // The queue has changed - it's worth sorting and de-duping
\r
390 std::sort(m_foldersToUpdate.begin(), m_foldersToUpdate.end());
\r
391 m_foldersToUpdate.erase(std::unique(m_foldersToUpdate.begin(), m_foldersToUpdate.end(), &CTGitPath::PredLeftEquivalentToRight), m_foldersToUpdate.end());
\r
392 m_bItemsAddedSinceLastCrawl = false;
\r
394 // create a new CTGitPath object to make sure the cached flags are requested again.
\r
395 // without this, a missing file/folder is still treated as missing even if it is available
\r
396 // now when crawling.
\r
397 workingPath = CTGitPath(m_foldersToUpdate.front().GetWinPath());
\r
398 workingPath.SetCustomData(m_foldersToUpdate.front().GetCustomData());
\r
399 if ((DWORD(workingPath.GetCustomData()) < currentTicks)||(DWORD(workingPath.GetCustomData()) > (currentTicks + 200000)))
\r
400 m_foldersToUpdate.pop_front();
\r
403 // since we always sort the whole list, we risk adding tons of new paths to m_pathsToUpdate
\r
404 // until the last one in the sorted list finally times out.
\r
405 // to avoid that, we go through the list again and crawl the first one which is valid
\r
406 // to crawl. That way, we still reduce the size of the list.
\r
407 if (m_foldersToUpdate.size() > 10)
\r
408 ATLTRACE("attention: the list of folders to update is now %ld big!\n", m_foldersToUpdate.size());
\r
409 for (std::deque<CTGitPath>::iterator it = m_foldersToUpdate.begin(); it != m_foldersToUpdate.end(); ++it)
\r
412 if ((DWORD(workingPath.GetCustomData()) < currentTicks)||(DWORD(workingPath.GetCustomData()) > (currentTicks + 200000)))
\r
414 m_foldersToUpdate.erase(it);
\r
420 if (DWORD(workingPath.GetCustomData()) >= currentTicks)
\r
425 if ((!m_blockedPath.IsEmpty())&&(m_blockedPath.IsAncestorOf(workingPath)))
\r
427 if (!CGitStatusCache::Instance().IsPathAllowed(workingPath))
\r
430 ATLTRACE(_T("Crawling folder: %s\n"), workingPath.GetWinPath());
\r
432 AutoLocker print(critSec);
\r
433 _stprintf_s(szCurrentCrawledPath[nCurrentCrawledpathIndex], MAX_CRAWLEDPATHSLEN, _T("Crawling folder: %s"), workingPath.GetWinPath());
\r
434 nCurrentCrawledpathIndex++;
\r
435 if (nCurrentCrawledpathIndex >= MAX_CRAWLEDPATHS)
\r
436 nCurrentCrawledpathIndex = 0;
\r
438 InvalidateRect(hWnd, NULL, FALSE);
\r
439 CGitStatusCache::Instance().WaitToRead();
\r
440 // Now, we need to visit this folder, to make sure that we know its 'most important' status
\r
441 CCachedDirectory * cachedDir = CGitStatusCache::Instance().GetDirectoryCacheEntry(workingPath.GetDirectory());
\r
442 // check if the path is monitored by the watcher. If it isn't, then we have to invalidate the cache
\r
443 // for that path and add it to the watcher.
\r
444 if (!CGitStatusCache::Instance().IsPathWatched(workingPath))
\r
446 if (workingPath.HasAdminDir())
\r
447 CGitStatusCache::Instance().AddPathToWatch(workingPath);
\r
449 cachedDir->Invalidate();
\r
452 CGitStatusCache::Instance().Done();
\r
453 CGitStatusCache::Instance().WaitToWrite();
\r
454 CGitStatusCache::Instance().RemoveCacheForPath(workingPath);
\r
458 cachedDir->RefreshStatus(bRecursive);
\r
460 // While refreshing the status, we could get another crawl request for the same folder.
\r
461 // This can happen if the crawled folder has a lower status than one of the child folders
\r
462 // (recursively). To avoid double crawlings, remove such a crawl request here
\r
463 AutoLocker lock(m_critSec);
\r
464 if (m_bItemsAddedSinceLastCrawl)
\r
466 if (m_foldersToUpdate.back().IsEquivalentToWithoutCase(workingPath))
\r
468 m_foldersToUpdate.pop_back();
\r
469 m_bItemsAddedSinceLastCrawl = false;
\r
472 CGitStatusCache::Instance().Done();
\r
479 bool CFolderCrawler::SetHoldoff(DWORD milliseconds /* = 100*/)
\r
481 long tick = (long)GetTickCount();
\r
482 bool ret = ((tick - m_crawlHoldoffReleasesAt) > 0);
\r
483 m_crawlHoldoffReleasesAt = tick + milliseconds;
\r
487 void CFolderCrawler::BlockPath(const CTGitPath& path, DWORD ticks)
\r
489 ATLTRACE(_T("block path %s from being crawled\n"), path.GetWinPath());
\r
490 m_blockedPath = path;
\r
492 m_blockReleasesAt = GetTickCount()+10000;
\r
494 m_blockReleasesAt = GetTickCount()+ticks;
\r