1
// TortoiseGit - a Windows shell extension for easy version control
3 // External Cache Copyright (C) 2005-2008, 2011-2012 - TortoiseSVN
4 // Copyright (C) 2008-2017, 2019-2023 - 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"
29 extern HWND hWndHidden
;
30 extern CGitAdminDirMap g_AdminDirMap
;
32 CDirectoryWatcher::CDirectoryWatcher()
34 // enable the required privileges for this process
36 LPCWSTR arPrivelegeNames
[] = { SE_BACKUP_NAME
,
41 for (int i
= 0; i
< (sizeof(arPrivelegeNames
) / sizeof(LPCWSTR
)); ++i
)
43 CAutoGeneralHandle hToken
;
44 if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES
, hToken
.GetPointer()))
46 TOKEN_PRIVILEGES tp
= { 1 };
48 if (LookupPrivilegeValue(nullptr, arPrivelegeNames
[i
], &tp
.Privileges
[0].Luid
))
50 tp
.Privileges
[0].Attributes
= SE_PRIVILEGE_ENABLED
;
52 AdjustTokenPrivileges(hToken
, FALSE
, &tp
, sizeof(tp
), nullptr, nullptr);
57 unsigned int threadId
= 0;
58 m_hThread
= reinterpret_cast<HANDLE
>(_beginthreadex(nullptr, 0, ThreadEntry
, this, 0, &threadId
));
61 CDirectoryWatcher::~CDirectoryWatcher()
64 AutoLocker
lock(m_critSec
);
69 void CDirectoryWatcher::CloseCompletionPort()
71 m_hCompPort
.CloseHandle();
74 void CDirectoryWatcher::ScheduleForDeletion (CDirWatchInfo
* info
)
76 infoToDelete
.push_back (info
);
79 void CDirectoryWatcher::CleanupWatchInfo()
81 AutoLocker
lock(m_critSec
);
82 InterlockedExchange(&m_bCleaned
, TRUE
);
83 while (!infoToDelete
.empty())
85 CDirWatchInfo
* info
= infoToDelete
.back();
86 infoToDelete
.pop_back();
91 void CDirectoryWatcher::Stop()
93 InterlockedExchange(&m_bRunning
, FALSE
);
95 WaitForSingleObject(m_hThread
, 4000);
96 m_hThread
.CloseHandle();
99 void CDirectoryWatcher::SetFolderCrawler(CFolderCrawler
* crawler
)
101 m_FolderCrawler
= crawler
;
104 bool CDirectoryWatcher::RemovePathAndChildren(const CTGitPath
& path
)
106 bool bRemoved
= false;
107 AutoLocker
lock(m_critSec
);
109 for (int i
=0; i
<watchedPaths
.GetCount(); ++i
)
111 if (path
.IsAncestorOf(watchedPaths
[i
]))
113 watchedPaths
.RemovePath(watchedPaths
[i
]);
121 void CDirectoryWatcher::BlockPath(const CTGitPath
& path
)
124 // block the path from being watched for 4 seconds
125 blockTickCount
= GetTickCount64() + 4000;
126 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Blocking path: %s\n", path
.GetWinPath());
129 bool CDirectoryWatcher::AddPath(const CTGitPath
& path
, bool bCloseInfoMap
)
131 if (!CGitStatusCache::Instance().IsPathAllowed(path
))
133 if ((!blockedPath
.IsEmpty())&&(blockedPath
.IsAncestorOf(path
)))
135 if (GetTickCount64() < blockTickCount
)
137 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Path %s prevented from being watched\n", path
.GetWinPath());
142 // ignore the recycle bin
143 PTSTR pFound
= StrStrI(path
.GetWinPath(), L
":\\RECYCLER");
146 if (*(pFound
+ wcslen(L
":\\RECYCLER")) == L
'\0' || *(pFound
+ wcslen(L
":\\RECYCLER")) == L
'\\')
149 pFound
= StrStrI(path
.GetWinPath(), L
":\\$Recycle.Bin");
152 if (*(pFound
+ wcslen(L
":\\$Recycle.Bin")) == L
'\0' || *(pFound
+ wcslen(L
":\\$Recycle.Bin")) == L
'\\')
156 AutoLocker
lock(m_critSec
);
157 for (int i
=0; i
<watchedPaths
.GetCount(); ++i
)
159 if (watchedPaths
[i
].IsAncestorOf(path
))
160 return false; // already watched (recursively)
163 // now check if with the new path we might have a new root
165 for (int i
=0; i
<watchedPaths
.GetCount(); ++i
)
167 const CString
& watched
= watchedPaths
[i
].GetWinPathString();
168 const CString
& sPath
= path
.GetWinPathString();
169 const int minlen
= min(sPath
.GetLength(), watched
.GetLength());
171 for (len
= 0; len
< minlen
; ++len
)
173 if (watched
.GetAt(len
) != sPath
.GetAt(len
))
175 if ((len
> 1)&&(len
< minlen
))
177 if (sPath
.GetAt(len
)=='\\')
178 newroot
= CTGitPath(sPath
.Left(len
));
179 else if (watched
.GetAt(len
)=='\\')
180 newroot
= CTGitPath(watched
.Left(len
));
187 if (sPath
.GetLength() == minlen
)
189 if (watched
.GetLength() > minlen
)
191 if (watched
.GetAt(len
)=='\\')
193 else if (sPath
.GetLength() == 3 && sPath
[1] == ':')
199 if (sPath
.GetLength() > minlen
)
201 if (sPath
.GetAt(len
)=='\\')
202 newroot
= CTGitPath(watched
);
203 else if (watched
.GetLength() == 3 && watched
[1] == ':')
204 newroot
= CTGitPath(watched
);
209 if (!newroot
.IsEmpty() && newroot
.HasAdminDir())
211 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": add path to watch %s\n", newroot
.GetWinPath());
212 watchedPaths
.AddPath(newroot
);
213 watchedPaths
.RemoveChildren();
219 if (!path
.HasAdminDir())
221 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": Path %s prevented from being watched: not versioned\n", path
.GetWinPath());
224 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": add path to watch %s\n", path
.GetWinPath());
225 watchedPaths
.AddPath(path
);
232 bool CDirectoryWatcher::IsPathWatched(const CTGitPath
& path
)
234 AutoLocker
lock(m_critSec
);
235 for (int i
=0; i
<watchedPaths
.GetCount(); ++i
)
237 if (watchedPaths
[i
].IsAncestorOf(path
))
243 unsigned int CDirectoryWatcher::ThreadEntry(void* pContext
)
245 reinterpret_cast<CDirectoryWatcher
*>(pContext
)->WorkerThread();
249 void CDirectoryWatcher::WorkerThread()
252 CDirWatchInfo
* pdi
= nullptr;
253 LPOVERLAPPED lpOverlapped
;
254 WCHAR buf
[READ_DIR_CHANGE_BUFFER_SIZE
] = {0};
255 WCHAR
* pFound
= nullptr;
259 if (!watchedPaths
.IsEmpty())
261 // Any incoming notifications?
265 InterlockedExchange(&m_bCleaned
, FALSE
);
267 || !GetQueuedCompletionStatus(m_hCompPort
,
271 600000 /*10 minutes*/))
278 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": restarting watcher\n");
279 m_hCompPort
.CloseHandle();
281 // We must sync the whole section because other threads may
282 // receive "AddPath" calls that will delete the completion
283 // port *while* we are adding references to it .
285 AutoLocker
lock(m_critSec
);
287 // Clear the list of watched objects and recreate that list.
288 // This will also delete the old completion port
293 for (int i
=0; i
<watchedPaths
.GetCount(); ++i
)
295 CTGitPath watchedPath
= watchedPaths
[i
];
297 CAutoFile hDir
= CreateFile(watchedPath
.GetWinPath(),
299 FILE_SHARE_READ
| FILE_SHARE_WRITE
| FILE_SHARE_DELETE
,
300 nullptr, //security attributes
302 FILE_FLAG_BACKUP_SEMANTICS
| //required privileges: SE_BACKUP_NAME and SE_RESTORE_NAME.
303 FILE_FLAG_OVERLAPPED
,
307 // this could happen if a watched folder has been removed/renamed
308 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": CreateFile failed. Can't watch directory %s\n", watchedPaths
[i
].GetWinPath());
309 watchedPaths
.RemovePath(watchedPath
);
313 DEV_BROADCAST_HANDLE NotificationFilter
= { 0 };
314 NotificationFilter
.dbch_size
= sizeof(DEV_BROADCAST_HANDLE
);
315 NotificationFilter
.dbch_devicetype
= DBT_DEVTYP_HANDLE
;
316 NotificationFilter
.dbch_handle
= hDir
;
317 // RegisterDeviceNotification sends a message to the UI thread:
318 // make sure we *can* send it and that the UI thread isn't waiting on a lock
319 const int numPaths
= watchedPaths
.GetCount();
320 const size_t numWatch
= watchInfoMap
.size();
322 NotificationFilter
.dbch_hdevnotify
= RegisterDeviceNotification(hWndHidden
, &NotificationFilter
, DEVICE_NOTIFY_WINDOW_HANDLE
);
324 // since we released the lock to prevent a deadlock with the UI thread,
325 // it could happen that new paths were added to watch, or another thread
326 // could have cleared our info map.
327 // if that happened, we have to restart watching all paths again.
328 if ((numPaths
!= watchedPaths
.GetCount()) || (numWatch
!= watchInfoMap
.size()))
336 CDirWatchInfo
* pDirInfo
= new CDirWatchInfo(hDir
.Detach(), watchedPath
);// the new CDirWatchInfo object owns the handle now
337 pDirInfo
->m_hDevNotify
= NotificationFilter
.dbch_hdevnotify
;
340 HANDLE port
= CreateIoCompletionPort(pDirInfo
->m_hDir
, m_hCompPort
, reinterpret_cast<ULONG_PTR
>(pDirInfo
), 0);
341 if (port
== INVALID_HANDLE_VALUE
)
343 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": CreateIoCompletionPort failed. Can't watch directory %s\n", watchedPath
.GetWinPath());
345 // we must close the directory handle to allow ClearInfoMap()
346 // to close the completion port properly
347 pDirInfo
->CloseDirectoryHandle();
354 watchedPaths
.RemovePath(watchedPath
);
357 m_hCompPort
= std::move(port
);
359 if (!ReadDirectoryChangesW(pDirInfo
->m_hDir
,
361 READ_DIR_CHANGE_BUFFER_SIZE
,
363 FILE_NOTIFY_CHANGE_FILE_NAME
| FILE_NOTIFY_CHANGE_DIR_NAME
| FILE_NOTIFY_CHANGE_LAST_WRITE
,
364 &numBytes
,// not used
365 &pDirInfo
->m_Overlapped
,
366 nullptr)) //no completion routine!
368 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": ReadDirectoryChangesW failed. Can't watch directory %s\n", watchedPath
.GetWinPath());
370 // we must close the directory handle to allow ClearInfoMap()
371 // to close the completion port properly
372 pDirInfo
->CloseDirectoryHandle();
378 watchedPaths
.RemovePath(watchedPath
);
382 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": watching path %s\n", pDirInfo
->m_DirName
.GetWinPath());
383 watchInfoMap
[pDirInfo
->m_hDir
] = pDirInfo
;
390 if (watchInfoMap
.empty())
393 // NOTE: the longer this code takes to execute until ReadDirectoryChangesW
394 // is called again, the higher the chance that we miss some
395 // changes in the file system!
399 std::list
<CTGitPath
> notifyPaths
;
401 AutoLocker
lock(m_critSec
);
402 // in case the CDirectoryWatcher objects have been cleaned,
403 // the m_bCleaned variable will be set to true here. If the
404 // objects haven't been cleared, we can access them here.
405 if (InterlockedExchange(&m_bCleaned
, FALSE
))
407 if ( (!pdi
->m_hDir
) || watchInfoMap
.empty()
408 || (watchInfoMap
.find(pdi
->m_hDir
) == watchInfoMap
.end()))
412 auto pnotify
= reinterpret_cast<PFILE_NOTIFY_INFORMATION
>(pdi
->m_Buffer
);
417 pnotify
= reinterpret_cast<PFILE_NOTIFY_INFORMATION
>(reinterpret_cast<LPBYTE
>(pnotify
) + nOffset
);
419 if (reinterpret_cast<ULONG_PTR
>(pnotify
) - reinterpret_cast<ULONG_PTR
>(pdi
->m_Buffer
) > READ_DIR_CHANGE_BUFFER_SIZE
)
422 nOffset
= pnotify
->NextEntryOffset
;
424 if (pnotify
->FileNameLength
>= (READ_DIR_CHANGE_BUFFER_SIZE
* sizeof(wchar_t)))
427 SecureZeroMemory(buf
, READ_DIR_CHANGE_BUFFER_SIZE
* sizeof(wchar_t));
428 wcsncpy_s(buf
, pdi
->m_DirPath
, _countof(buf
) - 1);
429 errno_t err
= wcsncat_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(), int(pnotify
->FileNameLength
/ sizeof(wchar_t))));
430 if (err
== STRUNCATE
)
432 buf
[(pnotify
->FileNameLength
/ sizeof(wchar_t)) + pdi
->m_DirPath
.GetLength()] = L
'\0';
436 if ((pFound
= StrStrI(buf
, L
"\\tmp")) != nullptr)
438 pFound
+= wcslen(L
"\\tmp");
439 if (*pFound
== L
'\\' || *pFound
== L
'\0')
442 if ((pFound
= StrStrI(buf
, L
":\\RECYCLER")) != nullptr)
444 if (*(pFound
+ wcslen(L
":\\RECYCLER")) == L
'\0' || *(pFound
+ wcslen(L
":\\RECYCLER")) == L
'\\')
447 if ((pFound
= StrStrI(buf
, L
":\\$Recycle.Bin")) != nullptr)
449 if (*(pFound
+ wcslen(L
":\\$Recycle.Bin")) == L
'\0' || *(pFound
+ wcslen(L
":\\$Recycle.Bin")) == L
'\\')
453 if (StrStrI(buf
, L
".tmp"))
455 // assume files with a .tmp extension are not versioned and interesting,
461 bool isIndex
= false;
462 if ((pFound
= wcsstr(buf
, L
".git")) != nullptr)
464 // omit repository data change except .git/index.lock- or .git/HEAD.lock-files
465 if (reinterpret_cast<ULONG_PTR
>(pnotify
) - reinterpret_cast<ULONG_PTR
>(pdi
->m_Buffer
) > READ_DIR_CHANGE_BUFFER_SIZE
)
468 path
= g_AdminDirMap
.GetWorkingCopy(CTGitPath(buf
).GetContainingDirectory().GetWinPathString());
470 if ((wcsstr(pFound
, L
"index.lock") || wcsstr(pFound
, L
"HEAD.lock")) && pnotify
->Action
== FILE_ACTION_ADDED
)
472 CGitStatusCache::Instance().BlockPath(path
);
475 else if (((wcsstr(pFound
, L
"index.lock") || wcsstr(pFound
, L
"HEAD.lock")) && pnotify
->Action
== FILE_ACTION_REMOVED
) || (((wcsstr(pFound
, L
"index") && !wcsstr(pFound
, L
"index.lock")) || (wcsstr(pFound
, L
"HEAD") && wcsstr(pFound
, L
"HEAD.lock"))) && pnotify
->Action
== FILE_ACTION_MODIFIED
) || ((!wcsstr(pFound
, L
"index.lock") || wcsstr(pFound
, L
"HEAD.lock")) && pnotify
->Action
== FILE_ACTION_RENAMED_NEW_NAME
))
478 CGitStatusCache::Instance().BlockPath(path
, 1);
484 path
.SetFromUnknown(buf
);
486 if(!path
.HasAdminDir() && !isIndex
)
489 CTraceToOutputDebugString::Instance()(_T(__FUNCTION__
) L
": change notification for %s\n", buf
);
490 notifyPaths
.push_back(path
);
492 } while ((nOffset
> 0)&&(nOffset
< READ_DIR_CHANGE_BUFFER_SIZE
));
494 // setup next notification cycle
495 SecureZeroMemory (pdi
->m_Buffer
, sizeof(pdi
->m_Buffer
));
496 SecureZeroMemory (&pdi
->m_Overlapped
, sizeof(OVERLAPPED
));
497 bRet
= ReadDirectoryChangesW(pdi
->m_hDir
,
499 READ_DIR_CHANGE_BUFFER_SIZE
,
501 FILE_NOTIFY_CHANGE_FILE_NAME
| FILE_NOTIFY_CHANGE_DIR_NAME
| FILE_NOTIFY_CHANGE_LAST_WRITE
,
502 &numBytes
,// not used
504 nullptr); //no completion routine!
506 if (!notifyPaths
.empty())
508 for (auto nit
= notifyPaths
.cbegin(); nit
!= notifyPaths
.cend(); ++nit
)
510 m_FolderCrawler
->AddPathForUpdate(*nit
);
514 // any clean-up to do?
520 // Since the call to ReadDirectoryChangesW failed, just
521 // wait a while. We don't want to have this thread
522 // running using 100% CPU if something goes completely
524 pdi
->CloseDirectoryHandle();
525 watchedPaths
.RemovePath(pdi
->m_DirName
);
527 CloseCompletionPort();
531 }// if (!watchedPaths.IsEmpty())
534 }// while (m_bRunning)
537 // call this before destroying async I/O structures:
539 void CDirectoryWatcher::CloseWatchHandles()
541 AutoLocker
lock(m_critSec
);
543 for (auto I
= watchInfoMap
.cbegin(); I
!= watchInfoMap
.cend(); ++I
)
545 I
->second
->CloseDirectoryHandle();
547 CloseCompletionPort();
550 void CDirectoryWatcher::ClearInfoMap()
553 if (!watchInfoMap
.empty())
555 AutoLocker
lock(m_critSec
);
556 for (TInfoMap::iterator I
= watchInfoMap
.begin(); I
!= watchInfoMap
.end(); ++I
)
558 CDirectoryWatcher::CDirWatchInfo
* info
= I
->second
;
560 ScheduleForDeletion (info
);
562 watchInfoMap
.clear();
566 CTGitPath
CDirectoryWatcher::CloseInfoMap(HANDLE hDir
)
569 AutoLocker
lock(m_critSec
);
570 TInfoMap::const_iterator d
= watchInfoMap
.find(hDir
);
571 if (d
!= watchInfoMap
.end())
573 path
= CTGitPath(CTGitPath(d
->second
->m_DirPath
).GetRootPathString());
574 RemovePathAndChildren(path
);
579 if (watchInfoMap
.empty())
582 for (auto I
= watchInfoMap
.cbegin(); I
!= watchInfoMap
.cend(); ++I
)
584 CDirectoryWatcher::CDirWatchInfo
* info
= I
->second
;
586 ScheduleForDeletion (info
);
588 watchInfoMap
.clear();
593 bool CDirectoryWatcher::CloseHandlesForPath(const CTGitPath
& path
)
595 AutoLocker
lock(m_critSec
);
598 if (watchInfoMap
.empty())
601 for (TInfoMap::iterator I
= watchInfoMap
.begin(); I
!= watchInfoMap
.end(); ++I
)
603 CDirectoryWatcher::CDirWatchInfo
* info
= I
->second
;
605 CTGitPath p
= CTGitPath(info
->m_DirPath
);
606 if (path
.IsAncestorOf(p
))
608 RemovePathAndChildren(p
);
611 ScheduleForDeletion(info
);
613 watchInfoMap
.clear();
617 CDirectoryWatcher::CDirWatchInfo::CDirWatchInfo(HANDLE hDir
, const CTGitPath
& DirectoryName
)
618 : m_hDir(std::move(hDir
))
619 , m_DirName(DirectoryName
)
621 ATLASSERT(m_hDir
&& !DirectoryName
.IsEmpty());
623 m_DirPath
= m_DirName
.GetWinPathString();
624 if (m_DirPath
.GetAt(m_DirPath
.GetLength() - 1) != L
'\\')
628 CDirectoryWatcher::CDirWatchInfo::~CDirWatchInfo()
630 CloseDirectoryHandle();
633 bool CDirectoryWatcher::CDirWatchInfo::CloseDirectoryHandle()
635 bool b
= m_hDir
.CloseHandle();
639 UnregisterDeviceNotification(m_hDevNotify
);
640 m_hDevNotify
= nullptr;