Guard the write access to the m_directoryCache member with a lock
[TortoiseGit.git] / src / TGitCache / GITStatusCache.cpp
blob66b616eff0b6d4aeb85b158a2a4ad130b73d7afc
1 // TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2006,2008,2010 - 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.
21 #include "stdafx.h"
22 #include "GitStatus.h"
23 #include "GitStatusCache.h"
24 #include "CacheInterface.h"
25 #include <ShlObj.h>
27 //////////////////////////////////////////////////////////////////////////
29 #define BLOCK_PATH_DEFAULT_TIMEOUT 600 // 10 minutes
30 #define BLOCK_PATH_MAX_TIMEOUT 1200 // 20 minutes
32 CGitStatusCache* CGitStatusCache::m_pInstance;
34 CGitStatusCache& CGitStatusCache::Instance()
36 ATLASSERT(m_pInstance != NULL);
37 return *m_pInstance;
40 void CGitStatusCache::Create()
42 ATLASSERT(m_pInstance == NULL);
43 m_pInstance = new CGitStatusCache;
45 m_pInstance->watcher.SetFolderCrawler(&m_pInstance->m_folderCrawler);
46 #define LOADVALUEFROMFILE(x) if (fread(&x, sizeof(x), 1, pFile)!=1) goto exit;
47 #define LOADVALUEFROMFILE2(x) if (fread(&x, sizeof(x), 1, pFile)!=1) goto error;
48 unsigned int value = (unsigned int)-1;
49 FILE * pFile = NULL;
50 // find the location of the cache
51 TCHAR path[MAX_PATH] = { 0 }; //MAX_PATH ok here.
52 TCHAR path2[MAX_PATH] = { 0 };
53 if (SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, path)==S_OK)
55 _tcscat_s(path, MAX_PATH, _T("\\TGitCache"));
56 if (!PathIsDirectory(path))
58 if (CreateDirectory(path, NULL)==0)
59 goto error;
61 _tcscat_s(path, MAX_PATH, _T("\\cache"));
62 // in case the cache file is corrupt, we could crash while
63 // reading it! To prevent crashing every time once that happens,
64 // we make a copy of the cache file and use that copy to read from.
65 // if that copy is corrupt, the original file won't exist anymore
66 // and the second time we start up and try to read the file,
67 // it's not there anymore and we start from scratch without a crash.
68 _tcscpy_s(path2, MAX_PATH, path);
69 _tcscat_s(path2, MAX_PATH, _T("2"));
70 DeleteFile(path2);
71 CopyFile(path, path2, FALSE);
72 DeleteFile(path);
73 pFile = _tfsopen(path2, _T("rb"), _SH_DENYNO);
74 if (pFile)
76 try
78 LOADVALUEFROMFILE(value);
79 if (value != 2)
81 goto error;
83 int mapsize = 0;
84 LOADVALUEFROMFILE(mapsize);
85 for (int i=0; i<mapsize; ++i)
87 LOADVALUEFROMFILE2(value);
88 if (value > MAX_PATH)
89 goto error;
90 if (value)
92 CString sKey;
93 if (fread(sKey.GetBuffer(value+1), sizeof(TCHAR), value, pFile)!=value)
95 sKey.ReleaseBuffer(0);
96 goto error;
98 sKey.ReleaseBuffer(value);
99 std::unique_ptr<CCachedDirectory> cacheddir (new CCachedDirectory());
100 if (!cacheddir.get() || !cacheddir->LoadFromDisk(pFile))
102 cacheddir.reset();
103 goto error;
105 CTGitPath KeyPath = CTGitPath(sKey);
106 if (m_pInstance->IsPathAllowed(KeyPath))
108 // only add the path to the watch list if it is versioned
109 if ((cacheddir->GetCurrentFullStatus() != git_wc_status_unversioned)&&(cacheddir->GetCurrentFullStatus() != git_wc_status_none))
110 m_pInstance->watcher.AddPath(KeyPath, false);
112 m_pInstance->m_directoryCache[KeyPath] = cacheddir.release();
114 // do *not* add the paths for crawling!
115 // because crawled paths will trigger a shell
116 // notification, which makes the desktop flash constantly
117 // until the whole first time crawling is over
118 // m_pInstance->AddFolderForCrawling(KeyPath);
123 catch (CAtlException)
125 goto error;
129 exit:
130 if (pFile)
131 fclose(pFile);
132 DeleteFile(path2);
133 m_pInstance->watcher.ClearInfoMap();
134 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": cache loaded from disk successfully!\n");
135 return;
136 error:
137 if (pFile)
138 fclose(pFile);
139 DeleteFile(path2);
140 m_pInstance->watcher.ClearInfoMap();
141 Destroy();
142 m_pInstance = new CGitStatusCache;
143 CTraceToOutputDebugString::Instance()(__FUNCTION__ ": cache not loaded from disk\n");
146 bool CGitStatusCache::SaveCache()
148 #define WRITEVALUETOFILE(x) if (fwrite(&x, sizeof(x), 1, pFile)!=1) goto error;
149 unsigned int value = 0;
150 // save the cache to disk
151 FILE * pFile = NULL;
152 // find a location to write the cache to
153 TCHAR path[MAX_PATH] = { 0 }; //MAX_PATH ok here.
154 if (SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, path)==S_OK)
156 _tcscat_s(path, MAX_PATH, _T("\\TGitCache"));
157 if (!PathIsDirectory(path))
158 CreateDirectory(path, NULL);
159 _tcscat_s(path, MAX_PATH, _T("\\cache"));
160 _tfopen_s(&pFile, path, _T("wb"));
161 if (pFile)
163 value = 2; // 'version'
164 WRITEVALUETOFILE(value);
165 value = (int)m_pInstance->m_directoryCache.size();
166 WRITEVALUETOFILE(value);
167 for (CCachedDirectory::CachedDirMap::iterator I = m_pInstance->m_directoryCache.begin(); I != m_pInstance->m_directoryCache.end(); ++I)
169 if (I->second == NULL)
171 value = 0;
172 WRITEVALUETOFILE(value);
173 continue;
175 const CString& key = I->first.GetWinPathString();
176 value = key.GetLength();
177 WRITEVALUETOFILE(value);
178 if (value)
180 if (fwrite((LPCTSTR)key, sizeof(TCHAR), value, pFile)!=value)
181 goto error;
182 if (!I->second->SaveToDisk(pFile))
183 goto error;
186 fclose(pFile);
189 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": cache saved to disk at %s\n"), path);
190 return true;
191 error:
192 fclose(pFile);
193 Destroy();
194 DeleteFile(path);
195 return false;
198 void CGitStatusCache::Destroy()
200 if (m_pInstance)
202 m_pInstance->Stop();
203 Sleep(100);
204 delete m_pInstance;
205 m_pInstance = NULL;
209 void CGitStatusCache::Stop()
211 // m_svnHelp.Cancel(true);
212 watcher.Stop();
213 m_folderCrawler.Stop();
214 m_shellUpdater.Stop();
217 void CGitStatusCache::Init()
219 m_folderCrawler.Initialise();
220 m_shellUpdater.Initialise();
223 CGitStatusCache::CGitStatusCache(void)
225 #define forever DWORD(-1)
226 AutoLocker lock(m_NoWatchPathCritSec);
227 TCHAR path[MAX_PATH] = { 0 };
228 SHGetFolderPath(NULL, CSIDL_COOKIES, NULL, 0, path);
229 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
230 SecureZeroMemory(path, sizeof(path));
231 SHGetFolderPath(NULL, CSIDL_HISTORY, NULL, 0, path);
232 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
233 SecureZeroMemory(path, sizeof(path));
234 SHGetFolderPath(NULL, CSIDL_INTERNET_CACHE, NULL, 0, path);
235 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
236 SecureZeroMemory(path, sizeof(path));
237 SHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, 0, path);
238 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
239 SecureZeroMemory(path, sizeof(path));
240 SHGetFolderPath(NULL, CSIDL_WINDOWS, NULL, 0, path);
241 m_NoWatchPaths[CTGitPath(CString(path))] = forever;
242 m_bClearMemory = false;
243 m_mostRecentExpiresAt = 0;
246 CGitStatusCache::~CGitStatusCache(void)
248 CAutoWriteLock writeLock(m_guard);
249 ClearCache();
252 void CGitStatusCache::Refresh()
254 m_shellCache.ForceRefresh();
255 // m_pInstance->m_svnHelp.ReloadConfig();
256 if (!m_pInstance->m_directoryCache.empty())
258 CCachedDirectory::CachedDirMap::iterator I = m_pInstance->m_directoryCache.begin();
259 for (/* no init */; I != m_pInstance->m_directoryCache.end(); ++I)
261 if (m_shellCache.IsPathAllowed(I->first.GetWinPath()))
262 I->second->RefreshMostImportant();
263 else
265 CGitStatusCache::Instance().RemoveCacheForPath(I->first);
266 I = m_pInstance->m_directoryCache.begin();
267 if (I == m_pInstance->m_directoryCache.end())
268 break;
274 bool CGitStatusCache::IsPathGood(const CTGitPath& path)
276 AutoLocker lock(m_NoWatchPathCritSec);
277 for (std::map<CTGitPath, DWORD>::const_iterator it = m_NoWatchPaths.begin(); it != m_NoWatchPaths.end(); ++it)
279 // the ticks check is necessary here, because RemoveTimedoutBlocks is only called within the FolderCrawler loop
280 // and we might miss update calls
281 // TODO: maybe we also need to check if index.lock and HEAD.lock still exists after a specific timeout (if we miss update notifications for these files too often)
282 if (GetTickCount() < it->second && it->first.IsAncestorOf(path))
284 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": path not good: %s\n"), it->first.GetWinPath());
285 return false;
288 return true;
291 bool CGitStatusCache::BlockPath(const CTGitPath& path, DWORD timeout /* = 0 */)
293 if (timeout == 0)
294 timeout = BLOCK_PATH_DEFAULT_TIMEOUT;
296 if (timeout > BLOCK_PATH_MAX_TIMEOUT)
297 timeout = BLOCK_PATH_MAX_TIMEOUT;
299 timeout = GetTickCount() + (timeout * 1000); // timeout is in seconds, but we need the milliseconds
301 AutoLocker lock(m_NoWatchPathCritSec);
302 m_NoWatchPaths[path.GetDirectory()] = timeout;
304 return true;
307 bool CGitStatusCache::UnBlockPath(const CTGitPath& path)
309 bool ret = false;
310 AutoLocker lock(m_NoWatchPathCritSec);
311 std::map<CTGitPath, DWORD>::iterator it = m_NoWatchPaths.find(path.GetDirectory());
312 if (it != m_NoWatchPaths.end())
314 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": path removed from no good: %s\n"), it->first.GetWinPath());
315 m_NoWatchPaths.erase(it);
316 ret = true;
318 AddFolderForCrawling(path.GetDirectory());
320 return ret;
323 bool CGitStatusCache::RemoveTimedoutBlocks()
325 bool ret = false;
326 DWORD currentTicks = GetTickCount();
327 AutoLocker lock(m_NoWatchPathCritSec);
328 std::vector<CTGitPath> toRemove;
329 for (std::map<CTGitPath, DWORD>::const_iterator it = m_NoWatchPaths.begin(); it != m_NoWatchPaths.end(); ++it)
331 if (currentTicks > it->second)
333 toRemove.push_back(it->first);
336 if (!toRemove.empty())
338 for (std::vector<CTGitPath>::const_iterator it = toRemove.begin(); it != toRemove.end(); ++it)
340 ret = ret || UnBlockPath(*it);
344 return ret;
347 void CGitStatusCache::UpdateShell(const CTGitPath& path)
349 m_shellUpdater.AddPathForUpdate(path);
352 void CGitStatusCache::ClearCache()
354 CAutoWriteLock writeLock(m_guard);
355 for (CCachedDirectory::CachedDirMap::iterator I = m_directoryCache.begin(); I != m_directoryCache.end(); ++I)
357 delete I->second;
358 I->second = NULL;
360 m_directoryCache.clear();
363 bool CGitStatusCache::RemoveCacheForDirectory(CCachedDirectory * cdir)
365 if (cdir == NULL)
366 return false;
368 typedef std::map<CTGitPath, git_wc_status_kind> ChildDirStatus;
369 CAutoWriteLock writeLock(m_guard);
370 if (!cdir->m_childDirectories.empty())
372 ChildDirStatus::iterator it = cdir->m_childDirectories.begin();
373 for (; it != cdir->m_childDirectories.end(); )
375 CCachedDirectory * childdir = CGitStatusCache::Instance().GetDirectoryCacheEntryNoCreate(it->first);
376 if ((childdir) && (!cdir->m_directoryPath.IsEquivalentTo(childdir->m_directoryPath)) && (cdir->m_directoryPath.GetFileOrDirectoryName() != L".."))
377 RemoveCacheForDirectory(childdir);
378 cdir->m_childDirectories.erase(it->first);
379 it = cdir->m_childDirectories.begin();
382 cdir->m_childDirectories.clear();
383 m_directoryCache.erase(cdir->m_directoryPath);
385 // we could have entries versioned and/or stored in our cache which are
386 // children of the specified directory, but not in the m_childDirectories
387 // member
388 CCachedDirectory::ItDir itMap = m_directoryCache.lower_bound(cdir->m_directoryPath);
391 if (itMap != m_directoryCache.end())
393 if (cdir->m_directoryPath.IsAncestorOf(itMap->first))
395 // just in case (see TortoiseSVN issue #255)
396 if (itMap->second == cdir)
398 m_directoryCache.erase(itMap);
400 else
401 RemoveCacheForDirectory(itMap->second);
404 itMap = m_directoryCache.lower_bound(cdir->m_directoryPath);
405 } while (itMap != m_directoryCache.end() && cdir->m_directoryPath.IsAncestorOf(itMap->first));
407 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": removed from cache %s\n"), cdir->m_directoryPath.GetWinPathString());
408 delete cdir;
409 return true;
412 void CGitStatusCache::RemoveCacheForPath(const CTGitPath& path)
414 // Stop the crawler starting on a new folder
415 CCrawlInhibitor crawlInhibit(&m_folderCrawler);
416 CCachedDirectory::ItDir itMap;
417 CCachedDirectory * dirtoremove = NULL;
419 itMap = m_directoryCache.find(path);
420 if ((itMap != m_directoryCache.end())&&(itMap->second))
421 dirtoremove = itMap->second;
422 if (dirtoremove == NULL)
423 return;
424 ATLASSERT(path.IsEquivalentToWithoutCase(dirtoremove->m_directoryPath));
425 RemoveCacheForDirectory(dirtoremove);
428 CCachedDirectory * CGitStatusCache::GetDirectoryCacheEntry(const CTGitPath& path, bool isAddToWatch)
430 ATLASSERT(path.IsDirectory() || !PathFileExists(path.GetWinPath()));
433 CCachedDirectory::ItDir itMap;
434 itMap = m_directoryCache.find(path);
435 if ((itMap != m_directoryCache.end())&&(itMap->second))
437 // We've found this directory in the cache
438 return itMap->second;
440 else
442 // if the CCachedDirectory is NULL but the path is in our cache,
443 // that means that path got invalidated and needs to be treated
444 // as if it never was in our cache. So we remove the last remains
445 // from the cache and start from scratch.
447 CAutoWriteLock writeLock(m_guard);
448 // Since above there's a small chance that before we can upgrade to
449 // writer state some other thread gained writer state and changed
450 // the data, we have to recreate the iterator here again.
451 itMap = m_directoryCache.find(path);
452 if (itMap!=m_directoryCache.end())
454 delete itMap->second;
455 m_directoryCache.erase(itMap);
456 itMap->second = NULL;
458 // We don't know anything about this directory yet - lets add it to our cache
459 // but only if it exists!
460 if (path.Exists() && m_shellCache.IsPathAllowed(path.GetWinPath()) && !g_GitAdminDir.IsAdminDirPath(path.GetWinPath()))
462 // some notifications are for files which got removed/moved around.
463 // In such cases, the CTGitPath::IsDirectory() will return true (it assumes a directory if
464 // the path doesn't exist). Which means we can get here with a path to a file
465 // instead of a directory.
466 // Since we're here most likely called from the crawler thread, the file could exist
467 // again. If that's the case, just do nothing
468 if (path.IsDirectory()||(!path.Exists()))
470 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": adding %s to our cache\n"), path.GetWinPath());
471 CCachedDirectory * newcdir = new CCachedDirectory(path);
472 if (newcdir)
474 CCachedDirectory * cdir = m_directoryCache.insert(m_directoryCache.lower_bound(path), std::make_pair(path, newcdir))->second;
475 CString gitdir;
476 if ((!path.IsEmpty())&&(path.HasAdminDir(&gitdir))&&isAddToWatch)
478 /* Just watch version path */
479 watcher.AddPath(gitdir);
480 watcher.AddPath(path);
482 return cdir;
484 m_bClearMemory = true;
487 return NULL;
491 CCachedDirectory * CGitStatusCache::GetDirectoryCacheEntryNoCreate(const CTGitPath& path)
493 ATLASSERT(path.IsDirectory() || !PathFileExists(path.GetWinPath()));
495 CCachedDirectory::ItDir itMap;
496 itMap = m_directoryCache.find(path);
497 if(itMap != m_directoryCache.end())
499 // We've found this directory in the cache
500 return itMap->second;
502 return NULL;
505 /* Fetch is true, means fetch status from */
506 /* Fetch is false, means fetch status from cache */
507 CStatusCacheEntry CGitStatusCache::GetStatusForPath(const CTGitPath& path, DWORD flags, bool bFetch /* = true */)
509 bool bRecursive = !!(flags & TGITCACHE_FLAGS_RECUSIVE_STATUS);
511 // Check a very short-lived 'mini-cache' of the last thing we were asked for.
512 long now = (long)GetTickCount();
513 if(now-m_mostRecentExpiresAt < 0)
515 if(path.IsEquivalentToWithoutCase(m_mostRecentPath))
517 return m_mostRecentStatus;
521 AutoLocker lock(m_critSec);
522 m_mostRecentPath = path;
523 m_mostRecentExpiresAt = now + 1000;
526 if (IsPathGood(path))
528 // Stop the crawler starting on a new folder while we're doing this much more important task...
529 // Please note, that this may be a second "lock" used concurrently to the one in RemoveCacheForPath().
530 CCrawlInhibitor crawlInhibit(&m_folderCrawler);
532 CTGitPath dirpath = path.GetContainingDirectory();
533 if ((dirpath.IsEmpty()) || (!m_shellCache.IsPathAllowed(dirpath.GetWinPath())))
534 dirpath = path.GetDirectory();
535 CCachedDirectory * cachedDir = GetDirectoryCacheEntry(dirpath);
536 if (cachedDir != NULL)
538 //CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": GetStatusForMember %d\n"), bFetch);
539 CStatusCacheEntry entry = cachedDir->GetStatusForMember(path, bRecursive, bFetch);
541 AutoLocker lock(m_critSec);
542 m_mostRecentStatus = entry;
543 return m_mostRecentStatus;
547 else
549 // path is blocked for some reason: return the cached status if we have one
550 // we do here only a cache search, absolutely no disk access is allowed!
551 CCachedDirectory::ItDir itMap = m_directoryCache.find(path.GetDirectory());
552 if ((itMap != m_directoryCache.end())&&(itMap->second))
554 if (path.IsDirectory())
556 CStatusCacheEntry entry = itMap->second->GetOwnStatus(false);
557 AutoLocker lock(m_critSec);
558 m_mostRecentStatus = entry;
559 return m_mostRecentStatus;
561 else
563 // We've found this directory in the cache
564 CCachedDirectory * cachedDir = itMap->second;
565 CStatusCacheEntry entry = cachedDir->GetCacheStatusForMember(path);
567 AutoLocker lock(m_critSec);
568 m_mostRecentStatus = entry;
569 return m_mostRecentStatus;
574 AutoLocker lock(m_critSec);
575 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": ignored no good path %s\n"), path.GetWinPath());
576 m_mostRecentStatus = CStatusCacheEntry();
577 if (m_shellCache.ShowExcludedAsNormal() && path.IsDirectory() && m_shellCache.HasGITAdminDir(path.GetWinPath(), true))
579 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__) _T(": force status %s\n"), path.GetWinPath());
580 m_mostRecentStatus.ForceStatus(git_wc_status_normal);
582 return m_mostRecentStatus;
585 void CGitStatusCache::AddFolderForCrawling(const CTGitPath& path)
587 m_folderCrawler.AddDirectoryForUpdate(path);
590 void CGitStatusCache::CloseWatcherHandles(HANDLE hFile)
592 CTGitPath path = watcher.CloseInfoMap(hFile);
593 if (!path.IsEmpty())
594 m_folderCrawler.BlockPath(path);
595 CGitStatusCache::Instance().m_GitStatus.ReleasePathsRecursively(path.GetWinPathString());
598 void CGitStatusCache::CloseWatcherHandles(const CTGitPath& path)
600 watcher.CloseHandlesForPath(path);
601 m_folderCrawler.ReleasePathForUpdate(path);
602 CGitStatusCache::Instance().m_GitStatus.ReleasePathsRecursively(path.GetWinPathString());